//! Account information.
#![cfg_attr(docsrs, feature(doc_auto_cfg))]
#![allow(unsafe_code)]
#![allow(clippy::ptr_as_ptr)]

use std::{
    cell::{Ref, RefCell, RefMut},
    fmt,
    rc::Rc,
    slice::from_raw_parts_mut,
};

// TODO: remove `target_arch` once ebpf is removed
#[cfg(target_arch = "riscv64")]
use rialo_s_define_syscall::definitions::rlo_load_account;
use rialo_s_program_error::ProgramError;
use rialo_s_program_memory::rlo_memset;
use rialo_s_pubkey::Pubkey;
pub mod debug_account_data;

/// Maximum number of bytes a program may add to an account during a single realloc
pub const MAX_PERMITTED_DATA_INCREASE: usize = 1_024 * 10;

/// Account information
#[derive(Clone)]
#[repr(C)]
pub struct AccountInfo<'a> {
    /// Public key of the account
    pub key: &'a Pubkey,
    /// The kelvins in the account.  Modifiable by programs.
    pub kelvins: Rc<RefCell<&'a mut u64>>,
    /// The data held in this account.  Modifiable by programs.
    pub data: Rc<RefCell<&'a mut [u8]>>,
    /// Program that owns this account
    pub owner: &'a Pubkey,
    /// The epoch at which this account will next owe rent
    pub rent_epoch: u64,
    /// Was the transaction signed by this account's public key?
    pub is_signer: bool,
    /// Is the account writable?
    pub is_writable: bool,
    /// This account's data contains a loaded program (and is now read-only)
    pub executable: bool,
}

impl fmt::Debug for AccountInfo<'_> {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        let mut f = f.debug_struct("AccountInfo");

        f.field("key", &self.key)
            .field("owner", &self.owner)
            .field("is_signer", &self.is_signer)
            .field("is_writable", &self.is_writable)
            .field("executable", &self.executable)
            .field("rent_epoch", &self.rent_epoch)
            .field("kelvins", &self.kelvins())
            .field("data.len", &self.data_len());
        debug_account_data::debug_account_data(&self.data.borrow(), &mut f);

        f.finish_non_exhaustive()
    }
}

impl<'a> AccountInfo<'a> {
    pub fn signer_key(&self) -> Option<&Pubkey> {
        if self.is_signer {
            Some(self.key)
        } else {
            None
        }
    }

    pub fn unsigned_key(&self) -> &Pubkey {
        self.key
    }

    pub fn kelvins(&self) -> u64 {
        **self.kelvins.borrow()
    }

    pub fn try_kelvins(&self) -> Result<u64, ProgramError> {
        Ok(**self.try_borrow_kelvins()?)
    }

    /// Return the account's original data length when it was serialized for the
    /// current program invocation.
    ///
    /// # Safety
    ///
    /// This method assumes that the original data length was serialized as a u32
    /// integer in the 4 bytes immediately preceding the serialized account key.
    pub unsafe fn original_data_len(&self) -> usize {
        let key_ptr = self.key as *const _ as *const u8;
        let original_data_len_ptr = key_ptr.offset(-4) as *const u32;
        *original_data_len_ptr as usize
    }

    pub fn data_len(&self) -> usize {
        self.data.borrow().len()
    }

    pub fn try_data_len(&self) -> Result<usize, ProgramError> {
        Ok(self.try_borrow_data()?.len())
    }

    pub fn data_is_empty(&self) -> bool {
        self.data.borrow().is_empty()
    }

    pub fn try_data_is_empty(&self) -> Result<bool, ProgramError> {
        Ok(self.try_borrow_data()?.is_empty())
    }

    pub fn try_borrow_kelvins(&self) -> Result<Ref<'_, &mut u64>, ProgramError> {
        self.kelvins
            .try_borrow()
            .map_err(|_| ProgramError::AccountBorrowFailed)
    }

    pub fn try_borrow_mut_kelvins(&self) -> Result<RefMut<'_, &'a mut u64>, ProgramError> {
        self.kelvins
            .try_borrow_mut()
            .map_err(|_| ProgramError::AccountBorrowFailed)
    }

    pub fn try_borrow_data(&self) -> Result<Ref<'_, &mut [u8]>, ProgramError> {
        self.data
            .try_borrow()
            .map_err(|_| ProgramError::AccountBorrowFailed)
    }

    pub fn try_borrow_mut_data(&self) -> Result<RefMut<'_, &'a mut [u8]>, ProgramError> {
        self.data
            .try_borrow_mut()
            .map_err(|_| ProgramError::AccountBorrowFailed)
    }

    /// Realloc the account's data and optionally zero-initialize the new
    /// memory.
    ///
    /// Note:  Account data can be increased within a single call by up to
    /// `rialo_s_program::entrypoint::MAX_PERMITTED_DATA_INCREASE` bytes.
    ///
    /// Note: Memory used to grow is already zero-initialized upon program
    /// entrypoint and re-zeroing it wastes compute units.  If within the same
    /// call a program reallocs from larger to smaller and back to larger again
    /// the new space could contain stale data.  Pass `true` for `zero_init` in
    /// this case, otherwise compute units will be wasted re-zero-initializing.
    ///
    /// # Safety
    ///
    /// This method makes assumptions about the layout and location of memory
    /// referenced by `AccountInfo` fields. It should only be called for
    /// instances of `AccountInfo` that were created by the runtime and received
    /// in the `process_instruction` entrypoint of a program.
    pub fn realloc(&self, new_len: usize, zero_init: bool) -> Result<(), ProgramError> {
        let mut data = self.try_borrow_mut_data()?;
        let old_len = data.len();

        // Return early if length hasn't changed
        if new_len == old_len {
            return Ok(());
        }

        // Return early if the length increase from the original serialized data
        // length is too large and would result in an out of bounds allocation.
        let original_data_len = unsafe { self.original_data_len() };
        if new_len.saturating_sub(original_data_len) > MAX_PERMITTED_DATA_INCREASE {
            return Err(ProgramError::InvalidRealloc);
        }

        // realloc
        unsafe {
            let data_ptr = data.as_mut_ptr();

            // First set new length in the serialized data
            *(data_ptr.offset(-8) as *mut u64) = new_len as u64;

            // Then recreate the local slice with the new length
            *data = from_raw_parts_mut(data_ptr, new_len)
        }

        if zero_init {
            let len_increase = new_len.saturating_sub(old_len);
            if len_increase > 0 {
                rlo_memset(&mut data[old_len..], 0, len_increase);
            }
        }

        Ok(())
    }

    /// Resize the account's data: Either truncating or zero extending.
    ///
    /// Note:  Account data can be increased within a single call by up to
    /// `solana_program::entrypoint::MAX_PERMITTED_DATA_INCREASE` bytes.
    ///
    /// # Safety
    ///
    /// This method makes assumptions about the layout and location of memory
    /// referenced by `AccountInfo` fields. It should only be called for
    /// instances of `AccountInfo` that were created by the runtime and received
    /// in the `process_instruction` entrypoint of a program.
    pub fn resize(&self, new_len: usize) -> Result<(), ProgramError> {
        let mut data = self.try_borrow_mut_data()?;
        let old_len = data.len();

        // Return early if length hasn't changed
        if new_len == old_len {
            return Ok(());
        }

        // Return early if the length increase from the original serialized data
        // length is too large and would result in an out of bounds allocation.
        let original_data_len = unsafe { self.original_data_len() };
        if new_len.saturating_sub(original_data_len) > MAX_PERMITTED_DATA_INCREASE {
            return Err(ProgramError::InvalidRealloc);
        }

        // realloc
        unsafe {
            let data_ptr = data.as_mut_ptr();

            // First set new length in the serialized data
            *(data_ptr.offset(-8) as *mut u64) = new_len as u64;

            // Then recreate the local slice with the new length
            *data = from_raw_parts_mut(data_ptr, new_len)
        }

        let len_increase = new_len.saturating_sub(old_len);
        if len_increase > 0 {
            rlo_memset(&mut data[old_len..], 0, len_increase);
        }

        Ok(())
    }

    #[allow(invalid_reference_casting)]
    pub fn assign(&self, new_owner: &Pubkey) {
        // Set the non-mut owner field
        unsafe {
            std::ptr::write_volatile(
                self.owner as *const Pubkey as *mut [u8; 32],
                new_owner.to_bytes(),
            );
        }
    }

    pub fn new(
        key: &'a Pubkey,
        is_signer: bool,
        is_writable: bool,
        kelvins: &'a mut u64,
        data: &'a mut [u8],
        owner: &'a Pubkey,
        executable: bool,
        rent_epoch: u64,
    ) -> Self {
        Self {
            key,
            is_signer,
            is_writable,
            kelvins: Rc::new(RefCell::new(kelvins)),
            data: Rc::new(RefCell::new(data)),
            owner,
            executable,
            rent_epoch,
        }
    }

    #[cfg(feature = "bincode")]
    pub fn deserialize_data<T: serde::de::DeserializeOwned>(&self) -> Result<T, bincode::Error> {
        bincode::deserialize(&self.data.borrow())
    }

    #[cfg(feature = "bincode")]
    pub fn serialize_data<T: serde::Serialize>(&self, state: &T) -> Result<(), bincode::Error> {
        if bincode::serialized_size(state)? > self.data_len() as u64 {
            return Err(Box::new(bincode::ErrorKind::SizeLimit));
        }
        bincode::serialize_into(&mut self.data.borrow_mut()[..], state)
    }

    /// Attempt to load [`AccountInfo`] for `pubkey`.
    #[cfg(target_arch = "riscv64")]
    pub fn load_account(_pubkey: Pubkey) -> Option<AccountInfo<'a>> {
        #[cfg(not(target_os = "solana"))]
        {
            None
        }
        #[cfg(target_os = "solana")]
        {
            let mut account_ptr = 0u64;
            let result = unsafe {
                rlo_load_account(
                    &_pubkey as *const _ as *const u8,
                    &mut account_ptr as *mut u64 as *mut u8,
                )
            };

            match result {
                0 => unsafe { Some(deserialize_account_info(account_ptr as *mut u8, 0).0) },
                _ => None,
            }
        }
    }
}

#[cfg(target_os = "solana")]
#[allow(clippy::arithmetic_side_effects)]
#[inline(always)] // this reduces CU usage by half!
unsafe fn deserialize_account_info<'a>(
    input: *mut u8,
    mut offset: usize,
) -> (AccountInfo<'a>, usize) {
    offset += size_of::<u8>(); // ignore dup marker

    #[allow(clippy::cast_ptr_alignment)]
    let is_signer = *(input.add(offset) as *const u8) != 0;
    offset += size_of::<u8>();

    #[allow(clippy::cast_ptr_alignment)]
    let is_writable = *(input.add(offset) as *const u8) != 0;
    offset += size_of::<u8>();

    #[allow(clippy::cast_ptr_alignment)]
    let executable = *(input.add(offset) as *const u8) != 0;
    offset += size_of::<u8>();

    // The original data length is stored here because these 4 bytes were
    // originally only used for padding and served as a good location to
    // track the original size of the account data in a compatible way.
    let original_data_len_offset = offset;
    offset += size_of::<u32>();

    let key: &Pubkey = &*(input.add(offset) as *const Pubkey);
    offset += size_of::<Pubkey>();

    let owner: &Pubkey = &*(input.add(offset) as *const Pubkey);
    offset += size_of::<Pubkey>();

    #[allow(clippy::cast_ptr_alignment)]
    let kelvins = Rc::new(RefCell::new(&mut *(input.add(offset) as *mut u64)));
    offset += size_of::<u64>();

    #[allow(clippy::cast_ptr_alignment)]
    let data_len = *(input.add(offset) as *const u64) as usize;
    offset += size_of::<u64>();

    // Store the original data length for detecting invalid reallocations and
    // requires that MAX_PERMITTED_DATA_LENGTH fits in a u32
    *(input.add(original_data_len_offset) as *mut u32) = data_len as u32;

    let data = Rc::new(RefCell::new({
        from_raw_parts_mut(input.add(offset), data_len)
    }));
    offset += data_len + MAX_PERMITTED_DATA_INCREASE;
    offset += (offset as *const u8).align_offset(8); // padding

    #[allow(clippy::cast_ptr_alignment)]
    let rent_epoch = *(input.add(offset) as *const u64);
    offset += size_of::<u64>();

    (
        AccountInfo {
            key,
            is_signer,
            is_writable,
            kelvins,
            data,
            owner,
            executable,
            rent_epoch,
        },
        offset,
    )
}

/// Constructs an `AccountInfo` from self, used in conversion implementations.
pub trait IntoAccountInfo<'a> {
    fn into_account_info(self) -> AccountInfo<'a>;
}
impl<'a, T: IntoAccountInfo<'a>> From<T> for AccountInfo<'a> {
    fn from(src: T) -> Self {
        src.into_account_info()
    }
}

/// Provides information required to construct an `AccountInfo`, used in
/// conversion implementations.
pub trait Account {
    fn get(&mut self) -> (&mut u64, &mut [u8], &Pubkey, bool, u64);
}

/// Convert (&'a Pubkey, &'a mut T) where T: Account into an `AccountInfo`
impl<'a, T: Account> IntoAccountInfo<'a> for (&'a Pubkey, &'a mut T) {
    fn into_account_info(self) -> AccountInfo<'a> {
        let (key, account) = self;
        let (kelvins, data, owner, executable, rent_epoch) = account.get();
        AccountInfo::new(
            key, false, false, kelvins, data, owner, executable, rent_epoch,
        )
    }
}

/// Convert (&'a Pubkey, bool, &'a mut T)  where T: Account into an
/// `AccountInfo`.
impl<'a, T: Account> IntoAccountInfo<'a> for (&'a Pubkey, bool, &'a mut T) {
    fn into_account_info(self) -> AccountInfo<'a> {
        let (key, is_signer, account) = self;
        let (kelvins, data, owner, executable, rent_epoch) = account.get();
        AccountInfo::new(
            key, is_signer, false, kelvins, data, owner, executable, rent_epoch,
        )
    }
}

/// Convert &'a mut (Pubkey, T) where T: Account into an `AccountInfo`.
impl<'a, T: Account> IntoAccountInfo<'a> for &'a mut (Pubkey, T) {
    fn into_account_info(self) -> AccountInfo<'a> {
        let (ref key, account) = self;
        let (kelvins, data, owner, executable, rent_epoch) = account.get();
        AccountInfo::new(
            key, false, false, kelvins, data, owner, executable, rent_epoch,
        )
    }
}

/// Convenience function for accessing the next item in an [`AccountInfo`]
/// iterator.
///
/// This is simply a wrapper around [`Iterator::next`] that returns a
/// [`ProgramError`] instead of an option.
///
/// # Errors
///
/// Returns [`ProgramError::NotEnoughAccountKeys`] if there are no more items in
/// the iterator.
///
/// # Examples
///
/// ```
/// use rialo_s_program_error::ProgramResult;
/// use rialo_s_account_info::{AccountInfo, next_account_info};
/// use rialo_s_pubkey::Pubkey;
/// # use rialo_s_program_error::ProgramError;
///
/// pub fn process_instruction(
///     program_id: &Pubkey,
///     accounts: &[AccountInfo],
///     instruction_data: &[u8],
/// ) -> ProgramResult {
///     let accounts_iter = &mut accounts.iter();
///     let signer = next_account_info(accounts_iter)?;
///     let payer = next_account_info(accounts_iter)?;
///
///     // do stuff ...
///
///     Ok(())
/// }
/// # let p = Pubkey::new_unique();
/// # let l = &mut 0;
/// # let d = &mut [0u8];
/// # let a = AccountInfo::new(&p, false, false, l, d, &p, false, 0);
/// # let accounts = &[a.clone(), a];
/// # process_instruction(
/// #    &Pubkey::new_unique(),
/// #    accounts,
/// #    &[],
/// # )?;
/// # Ok::<(), ProgramError>(())
/// ```
pub fn next_account_info<'a, 'b, I: Iterator<Item = &'a AccountInfo<'b>>>(
    iter: &mut I,
) -> Result<I::Item, ProgramError> {
    iter.next().ok_or(ProgramError::NotEnoughAccountKeys)
}

/// Convenience function for accessing multiple next items in an [`AccountInfo`]
/// iterator.
///
/// Returns a slice containing the next `count` [`AccountInfo`]s.
///
/// # Errors
///
/// Returns [`ProgramError::NotEnoughAccountKeys`] if there are not enough items
/// in the iterator to satisfy the request.
///
/// # Examples
///
/// ```
/// use rialo_s_program_error::ProgramResult;
/// use rialo_s_account_info::{AccountInfo, next_account_info, next_account_infos};
/// use rialo_s_pubkey::Pubkey;
/// # use rialo_s_program_error::ProgramError;
///
/// pub fn process_instruction(
///     program_id: &Pubkey,
///     accounts: &[AccountInfo],
///     instruction_data: &[u8],
/// ) -> ProgramResult {
///     let accounts_iter = &mut accounts.iter();
///     let signer = next_account_info(accounts_iter)?;
///     let payer = next_account_info(accounts_iter)?;
///     let outputs = next_account_infos(accounts_iter, 3)?;
///
///     // do stuff ...
///
///     Ok(())
/// }
/// # let p = Pubkey::new_unique();
/// # let l = &mut 0;
/// # let d = &mut [0u8];
/// # let a = AccountInfo::new(&p, false, false, l, d, &p, false, 0);
/// # let accounts = &[a.clone(), a.clone(), a.clone(), a.clone(), a];
/// # process_instruction(
/// #    &Pubkey::new_unique(),
/// #    accounts,
/// #    &[],
/// # )?;
/// # Ok::<(), ProgramError>(())
/// ```
pub fn next_account_infos<'a, 'b: 'a>(
    iter: &mut std::slice::Iter<'a, AccountInfo<'b>>,
    count: usize,
) -> Result<&'a [AccountInfo<'b>], ProgramError> {
    let accounts = iter.as_slice();
    if accounts.len() < count {
        return Err(ProgramError::NotEnoughAccountKeys);
    }
    let (accounts, remaining) = accounts.split_at(count);
    *iter = remaining.iter();
    Ok(accounts)
}

impl<'a> AsRef<AccountInfo<'a>> for AccountInfo<'a> {
    fn as_ref(&self) -> &AccountInfo<'a> {
        self
    }
}

#[cfg(test)]
mod tests {
    use super::*;
    use crate::debug_account_data::{Hex, MAX_DEBUG_ACCOUNT_DATA};

    #[test]
    fn test_next_account_infos() {
        let k1 = Pubkey::new_unique();
        let k2 = Pubkey::new_unique();
        let k3 = Pubkey::new_unique();
        let k4 = Pubkey::new_unique();
        let k5 = Pubkey::new_unique();
        let l1 = &mut 0;
        let l2 = &mut 0;
        let l3 = &mut 0;
        let l4 = &mut 0;
        let l5 = &mut 0;
        let d1 = &mut [0u8];
        let d2 = &mut [0u8];
        let d3 = &mut [0u8];
        let d4 = &mut [0u8];
        let d5 = &mut [0u8];

        let infos = &[
            AccountInfo::new(&k1, false, false, l1, d1, &k1, false, 0),
            AccountInfo::new(&k2, false, false, l2, d2, &k2, false, 0),
            AccountInfo::new(&k3, false, false, l3, d3, &k3, false, 0),
            AccountInfo::new(&k4, false, false, l4, d4, &k4, false, 0),
            AccountInfo::new(&k5, false, false, l5, d5, &k5, false, 0),
        ];
        let infos_iter = &mut infos.iter();
        let info1 = next_account_info(infos_iter).unwrap();
        let info2_3_4 = next_account_infos(infos_iter, 3).unwrap();
        let info5 = next_account_info(infos_iter).unwrap();

        assert_eq!(k1, *info1.key);
        assert_eq!(k2, *info2_3_4[0].key);
        assert_eq!(k3, *info2_3_4[1].key);
        assert_eq!(k4, *info2_3_4[2].key);
        assert_eq!(k5, *info5.key);
    }

    #[test]
    fn test_account_info_as_ref() {
        let k = Pubkey::new_unique();
        let l = &mut 0;
        let d = &mut [0u8];
        let info = AccountInfo::new(&k, false, false, l, d, &k, false, 0);
        assert_eq!(info.key, info.as_ref().key);
    }

    #[test]
    fn test_account_info_debug_data() {
        let key = Pubkey::new_unique();
        let mut kelvins = 42;
        let mut data = vec![5; 80];
        let data_str = format!("{:?}", Hex(&data[..MAX_DEBUG_ACCOUNT_DATA]));
        let info = AccountInfo::new(&key, false, false, &mut kelvins, &mut data, &key, false, 0);
        assert_eq!(
            format!("{info:?}"),
            format!(
                "AccountInfo {{ \
                key: {}, \
                owner: {}, \
                is_signer: {}, \
                is_writable: {}, \
                executable: {}, \
                rent_epoch: {}, \
                kelvins: {}, \
                data.len: {}, \
                data: {}, .. }}",
                key,
                key,
                false,
                false,
                false,
                0,
                kelvins,
                data.len(),
                data_str,
            )
        );

        let mut data = vec![5; 40];
        let data_str = format!("{:?}", Hex(&data));
        let info = AccountInfo::new(&key, false, false, &mut kelvins, &mut data, &key, false, 0);
        assert_eq!(
            format!("{info:?}"),
            format!(
                "AccountInfo {{ \
                key: {}, \
                owner: {}, \
                is_signer: {}, \
                is_writable: {}, \
                executable: {}, \
                rent_epoch: {}, \
                kelvins: {}, \
                data.len: {}, \
                data: {}, .. }}",
                key,
                key,
                false,
                false,
                false,
                0,
                kelvins,
                data.len(),
                data_str,
            )
        );

        let mut data = vec![];
        let info = AccountInfo::new(&key, false, false, &mut kelvins, &mut data, &key, false, 0);
        assert_eq!(
            format!("{info:?}"),
            format!(
                "AccountInfo {{ \
                key: {}, \
                owner: {}, \
                is_signer: {}, \
                is_writable: {}, \
                executable: {}, \
                rent_epoch: {}, \
                kelvins: {}, \
                data.len: {}, .. }}",
                key,
                key,
                false,
                false,
                false,
                0,
                kelvins,
                data.len(),
            )
        );
    }
}
