//! AccountInfo represents a reference to AccountSharedData in either an AppendVec or the write cache.
//! AccountInfo is not persisted anywhere between program runs.
//! AccountInfo is purely runtime state.
//! Note that AccountInfo is saved to disk buckets during runtime, but disk buckets are recreated at startup.
use crate::{
    accounts_db::{AppendVecId, CACHE_VIRTUAL_OFFSET},
    accounts_index::{IsCached, ZeroLamport},
    append_vec::ALIGN_BOUNDARY_OFFSET,
};

/// offset within an append vec to account data
pub type Offset = usize;

/// bytes used to store this account in append vec
/// Note this max needs to be big enough to handle max data len of 10MB, which is a const
pub type StoredSize = u32;

/// specify where account data is located
#[derive(Debug)]
pub enum StorageLocation {
    AppendVec(AppendVecId, Offset),
    Cached,
}

impl StorageLocation {
    pub fn is_offset_equal(&self, other: &StorageLocation) -> bool {
        match self {
            StorageLocation::Cached => {
                matches!(other, StorageLocation::Cached) // technically, 2 cached entries match in offset
            }
            StorageLocation::AppendVec(_, offset) => {
                match other {
                    StorageLocation::Cached => {
                        false // 1 cached, 1 not
                    }
                    StorageLocation::AppendVec(_, other_offset) => other_offset == offset,
                }
            }
        }
    }
    pub fn is_store_id_equal(&self, other: &StorageLocation) -> bool {
        match self {
            StorageLocation::Cached => {
                matches!(other, StorageLocation::Cached) // 2 cached entries are same store id
            }
            StorageLocation::AppendVec(store_id, _) => {
                match other {
                    StorageLocation::Cached => {
                        false // 1 cached, 1 not
                    }
                    StorageLocation::AppendVec(other_store_id, _) => other_store_id == store_id,
                }
            }
        }
    }
}

/// how large the offset we store in AccountInfo is
/// Note this is a smaller datatype than 'Offset'
/// AppendVecs store accounts aligned to u64, so offset is always a multiple of 8 (sizeof(u64))
pub type OffsetReduced = u32;

#[derive(Default, Debug, PartialEq, Clone, Copy)]
pub struct AccountInfo {
    /// index identifying the append storage
    store_id: AppendVecId,

    /// offset = 'reduced_offset' / ALIGN_BOUNDARY_OFFSET into the storage
    /// Note this is a smaller type than 'Offset'
    reduced_offset: OffsetReduced,

    /// needed to track shrink candidacy in bytes. Used to update the number
    /// of alive bytes in an AppendVec as newer slots purge outdated entries
    /// Note that highest bits are used by ALL_FLAGS
    /// So, 'stored_size' is 'stored_size_mask' with ALL_FLAGS masked out.
    stored_size_mask: StoredSize,
}

/// These flags can be present in stored_size_mask to indicate additional info about the AccountInfo

/// presence of this flag in stored_size_mask indicates this account info references an account with zero lamports
const IS_ZERO_LAMPORT_FLAG: StoredSize = 1 << (StoredSize::BITS - 1);
/// presence of this flag in stored_size_mask indicates this account info references an account stored in the cache
const IS_CACHED_STORE_ID_FLAG: StoredSize = 1 << (StoredSize::BITS - 2);
const ALL_FLAGS: StoredSize = IS_ZERO_LAMPORT_FLAG | IS_CACHED_STORE_ID_FLAG;

impl ZeroLamport for AccountInfo {
    fn is_zero_lamport(&self) -> bool {
        self.stored_size_mask & IS_ZERO_LAMPORT_FLAG == IS_ZERO_LAMPORT_FLAG
    }
}

impl IsCached for AccountInfo {
    fn is_cached(&self) -> bool {
        self.stored_size_mask & IS_CACHED_STORE_ID_FLAG == IS_CACHED_STORE_ID_FLAG
    }
}

impl IsCached for StorageLocation {
    fn is_cached(&self) -> bool {
        matches!(self, StorageLocation::Cached)
    }
}

/// We have to have SOME value for store_id when we are cached
const CACHE_VIRTUAL_STORAGE_ID: AppendVecId = AppendVecId::MAX;

impl AccountInfo {
    pub fn new(storage_location: StorageLocation, stored_size: StoredSize, lamports: u64) -> Self {
        assert_eq!(stored_size & ALL_FLAGS, 0);
        let mut stored_size_mask = stored_size;
        let (store_id, raw_offset) = match storage_location {
            StorageLocation::AppendVec(store_id, offset) => (store_id, offset),
            StorageLocation::Cached => {
                stored_size_mask |= IS_CACHED_STORE_ID_FLAG;
                (CACHE_VIRTUAL_STORAGE_ID, CACHE_VIRTUAL_OFFSET)
            }
        };
        if lamports == 0 {
            stored_size_mask |= IS_ZERO_LAMPORT_FLAG;
        }
        let reduced_offset: OffsetReduced = (raw_offset / ALIGN_BOUNDARY_OFFSET) as OffsetReduced;
        let result = Self {
            store_id,
            reduced_offset,
            stored_size_mask,
        };
        assert_eq!(result.offset(), raw_offset, "illegal offset");
        result
    }

    pub fn store_id(&self) -> AppendVecId {
        // if the account is in a cached store, the store_id is meaningless
        assert!(!self.is_cached());
        self.store_id
    }

    pub fn offset(&self) -> Offset {
        (self.reduced_offset as Offset) * ALIGN_BOUNDARY_OFFSET
    }

    pub fn stored_size(&self) -> StoredSize {
        // elminate the special bit that indicates the info references an account with zero lamports
        self.stored_size_mask & !ALL_FLAGS
    }

    /// true iff store_id and offset match self AND self is not cached
    /// If self is cached, then store_id and offset are meaningless.
    pub fn matches_storage_location(&self, store_id: AppendVecId, offset: Offset) -> bool {
        // order is set for best short circuit
        self.store_id == store_id && self.offset() == offset && !self.is_cached()
    }

    pub fn storage_location(&self) -> StorageLocation {
        if self.is_cached() {
            StorageLocation::Cached
        } else {
            StorageLocation::AppendVec(self.store_id, self.offset())
        }
    }
}
#[cfg(test)]
mod test {
    use {super::*, crate::append_vec::MAXIMUM_APPEND_VEC_FILE_SIZE};

    #[test]
    fn test_limits() {
        for offset in [
            MAXIMUM_APPEND_VEC_FILE_SIZE as Offset,
            0,
            ALIGN_BOUNDARY_OFFSET,
            4 * ALIGN_BOUNDARY_OFFSET,
        ] {
            let info = AccountInfo::new(StorageLocation::AppendVec(0, offset), 0, 0);
            assert!(info.offset() == offset);
        }
    }

    #[test]
    #[should_panic(expected = "illegal offset")]
    fn test_alignment() {
        let offset = 1; // not aligned
        AccountInfo::new(StorageLocation::AppendVec(0, offset), 0, 0);
    }

    #[test]
    fn test_matches_storage_location() {
        let offset = 0;
        let id = 0;
        let info = AccountInfo::new(StorageLocation::AppendVec(id, offset), 0, 0);
        assert!(info.matches_storage_location(id, offset));

        // wrong offset
        let offset = ALIGN_BOUNDARY_OFFSET;
        assert!(!info.matches_storage_location(id, offset));

        // wrong id
        let offset = 0;
        let id = 1;
        assert!(!info.matches_storage_location(id, offset));

        // is cached
        let id = CACHE_VIRTUAL_STORAGE_ID;
        let info = AccountInfo::new(StorageLocation::Cached, 0, 0);
        assert!(!info.matches_storage_location(id, offset));
    }
}
