use crate::{
    buffer::LimitedVec,
    gas::ChargeError,
    pages::{PageU32Size, WasmPage, GEAR_PAGE_SIZE},
};
use alloc::{collections::BTreeSet, format};
use byteorder::{ByteOrder, LittleEndian};
use core::{
    fmt,
    fmt::Debug,
    iter,
    ops::{Deref, DerefMut, RangeInclusive},
};
use scale_info::{
    scale::{self, Decode, Encode, EncodeLike, Input, Output},
    TypeInfo,
};
#[derive(Clone, Copy, Encode, Decode)]
pub struct MemoryInterval {
    pub offset: u32,
    pub size: u32,
}
impl MemoryInterval {
    #[inline]
    pub fn to_bytes(&self) -> [u8; 8] {
        let mut bytes = [0u8; 8];
        LittleEndian::write_u32(&mut bytes[0..4], self.offset);
        LittleEndian::write_u32(&mut bytes[4..8], self.size);
        bytes
    }
    #[inline]
    pub fn try_from_bytes(bytes: &[u8]) -> Result<Self, &'static str> {
        if bytes.len() != 8 {
            return Err("bytes size != 8");
        }
        let offset = LittleEndian::read_u32(&bytes[0..4]);
        let size = LittleEndian::read_u32(&bytes[4..8]);
        Ok(MemoryInterval { offset, size })
    }
}
impl From<(u32, u32)> for MemoryInterval {
    fn from(val: (u32, u32)) -> Self {
        MemoryInterval {
            offset: val.0,
            size: val.1,
        }
    }
}
impl From<MemoryInterval> for (u32, u32) {
    fn from(val: MemoryInterval) -> Self {
        (val.offset, val.size)
    }
}
impl Debug for MemoryInterval {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        f.write_str(&format!(
            "[offset: {:#x}, size: {:#x}]",
            self.offset, self.size
        ))
    }
}
pub type PageBufInner = LimitedVec<u8, (), GEAR_PAGE_SIZE>;
#[derive(Clone, PartialEq, Eq, TypeInfo)]
pub struct PageBuf(PageBufInner);
impl Encode for PageBuf {
    fn size_hint(&self) -> usize {
        GEAR_PAGE_SIZE
    }
    fn encode_to<W: Output + ?Sized>(&self, dest: &mut W) {
        dest.write(self.0.inner())
    }
}
impl Decode for PageBuf {
    #[inline]
    fn decode<I: Input>(input: &mut I) -> Result<Self, scale::Error> {
        let mut buffer = PageBufInner::new_default();
        input.read(buffer.inner_mut())?;
        Ok(Self(buffer))
    }
}
impl EncodeLike for PageBuf {}
impl Debug for PageBuf {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        write!(
            f,
            "PageBuf({:?}..{:?})",
            &self.0.inner()[0..10],
            &self.0.inner()[GEAR_PAGE_SIZE - 10..GEAR_PAGE_SIZE]
        )
    }
}
impl Deref for PageBuf {
    type Target = [u8];
    fn deref(&self) -> &Self::Target {
        self.0.inner()
    }
}
impl DerefMut for PageBuf {
    fn deref_mut(&mut self) -> &mut Self::Target {
        self.0.inner_mut()
    }
}
impl PageBuf {
    pub fn new_zeroed() -> PageBuf {
        Self(PageBufInner::new_default())
    }
    pub fn from_inner(mut inner: PageBufInner) -> Self {
        inner.extend_with(0);
        Self(inner)
    }
}
pub type HostPointer = u64;
static_assertions::const_assert!(
    core::mem::size_of::<HostPointer>() >= core::mem::size_of::<usize>()
);
#[derive(Debug, Clone, Eq, PartialEq, derive_more::Display)]
pub enum MemoryError {
    #[display(fmt = "Trying to access memory outside wasm program memory")]
    AccessOutOfBounds,
}
pub trait Memory {
    type GrowError: Debug;
    fn grow(&mut self, pages: WasmPage) -> Result<(), Self::GrowError>;
    fn size(&self) -> WasmPage;
    fn write(&mut self, offset: u32, buffer: &[u8]) -> Result<(), MemoryError>;
    fn read(&self, offset: u32, buffer: &mut [u8]) -> Result<(), MemoryError>;
    fn get_buffer_host_addr(&mut self) -> Option<HostPointer> {
        if self.size() == 0.into() {
            None
        } else {
            unsafe { Some(self.get_buffer_host_addr_unsafe()) }
        }
    }
    unsafe fn get_buffer_host_addr_unsafe(&mut self) -> HostPointer;
}
#[derive(Debug)]
pub struct AllocationsContext {
    init_allocations: BTreeSet<WasmPage>,
    allocations: BTreeSet<WasmPage>,
    max_pages: WasmPage,
    static_pages: WasmPage,
}
#[must_use]
pub trait GrowHandler {
    fn before_grow_action(mem: &mut impl Memory) -> Self;
    fn after_grow_action(self, mem: &mut impl Memory);
}
pub struct NoopGrowHandler;
impl GrowHandler for NoopGrowHandler {
    fn before_grow_action(_mem: &mut impl Memory) -> Self {
        NoopGrowHandler
    }
    fn after_grow_action(self, _mem: &mut impl Memory) {}
}
#[derive(Debug, Clone, Eq, PartialEq, derive_more::Display)]
#[display(fmt = "Allocated memory pages or memory size are incorrect")]
pub struct IncorrectAllocationDataError;
#[derive(Debug, Clone, Eq, PartialEq, derive_more::Display, derive_more::From)]
pub enum AllocError {
    #[from]
    #[display(fmt = "{_0}")]
    IncorrectAllocationData(IncorrectAllocationDataError),
    #[display(fmt = "Trying to allocate more wasm program memory than allowed")]
    ProgramAllocOutOfBounds,
    #[display(fmt = "Page {_0} cannot be freed by the current program")]
    InvalidFree(u32),
    #[display(fmt = "Invalid range {_0}:{_1} for free_range")]
    InvalidFreeRange(u32, u32),
    #[from]
    #[display(fmt = "{_0}")]
    GasCharge(ChargeError),
}
impl AllocationsContext {
    pub fn new(
        allocations: BTreeSet<WasmPage>,
        static_pages: WasmPage,
        max_pages: WasmPage,
    ) -> Self {
        Self {
            init_allocations: allocations.clone(),
            allocations,
            max_pages,
            static_pages,
        }
    }
    pub fn is_init_page(&self, page: WasmPage) -> bool {
        self.init_allocations.contains(&page)
    }
    pub fn alloc<G: GrowHandler>(
        &mut self,
        pages: WasmPage,
        mem: &mut impl Memory,
        charge_gas_for_grow: impl FnOnce(WasmPage) -> Result<(), ChargeError>,
    ) -> Result<WasmPage, AllocError> {
        let mem_size = mem.size();
        let mut start = self.static_pages;
        let mut start_page = None;
        for &end in self.allocations.iter().chain(iter::once(&mem_size)) {
            let page_gap = end.sub(start).map_err(|_| IncorrectAllocationDataError)?;
            if page_gap >= pages {
                start_page = Some(start);
                break;
            }
            start = end.inc().map_err(|_| AllocError::ProgramAllocOutOfBounds)?;
        }
        let start = if let Some(start) = start_page {
            start
        } else {
            let start = self
                .allocations
                .last()
                .map(|last| last.inc().unwrap_or_else(|err| {
                    unreachable!("Cannot increment last allocation: {}, but we checked in loop above that it can be done", err)
                }))
                .unwrap_or(self.static_pages);
            let end = start
                .add(pages)
                .map_err(|_| AllocError::ProgramAllocOutOfBounds)?;
            if end > self.max_pages {
                return Err(AllocError::ProgramAllocOutOfBounds);
            }
            let extra_grow = end.sub(mem_size).unwrap_or_else(|err| {
                unreachable!(
                    "`mem_size` must be bigger than all allocations and static pages, but get {}",
                    err
                )
            });
            if extra_grow == WasmPage::zero() {
                unreachable!("`extra grow cannot be zero");
            }
            charge_gas_for_grow(extra_grow)?;
            let grow_handler = G::before_grow_action(mem);
            mem.grow(extra_grow)
                .unwrap_or_else(|err| unreachable!("Failed to grow memory: {:?}", err));
            grow_handler.after_grow_action(mem);
            start
        };
        let new_allocations = start
            .iter_count(pages)
            .unwrap_or_else(|err| unreachable!("`start` + `pages` is out of wasm memory: {}", err));
        self.allocations.extend(new_allocations);
        Ok(start)
    }
    pub fn free(&mut self, page: WasmPage) -> Result<(), AllocError> {
        if page < self.static_pages || page >= self.max_pages {
            return Err(AllocError::InvalidFree(page.0));
        }
        if !self.allocations.remove(&page) {
            return Err(AllocError::InvalidFree(page.0));
        }
        Ok(())
    }
    pub fn free_range(&mut self, range: RangeInclusive<WasmPage>) -> Result<(), AllocError> {
        if *range.start() < self.static_pages || *range.end() >= self.max_pages {
            return Err(AllocError::InvalidFreeRange(range.start().0, range.end().0));
        }
        self.allocations.retain(|p| !range.contains(p));
        Ok(())
    }
    pub fn into_parts(self) -> (WasmPage, BTreeSet<WasmPage>, BTreeSet<WasmPage>) {
        (self.static_pages, self.init_allocations, self.allocations)
    }
}
#[cfg(test)]
mod tests {
    use crate::pages::{GearPage, PageNumber};
    use super::*;
    use alloc::vec::Vec;
    #[test]
    fn page_number_addition() {
        let sum = GearPage(100).add(200.into()).unwrap();
        assert_eq!(sum, GearPage(300));
    }
    #[test]
    fn page_number_subtraction() {
        let subtraction = GearPage(299).sub(199.into()).unwrap();
        assert_eq!(subtraction, GearPage(100))
    }
    #[test]
    fn wasm_pages_to_gear_pages() {
        let wasm_pages: Vec<WasmPage> = [0u32, 10u32].iter().copied().map(WasmPage).collect();
        let gear_pages: Vec<u32> = wasm_pages
            .iter()
            .flat_map(|p| p.to_pages_iter::<GearPage>())
            .map(|p| p.0)
            .collect();
        let expectation = [0, 1, 2, 3, 40, 41, 42, 43];
        assert!(gear_pages.eq(&expectation));
    }
    #[test]
    fn page_buf() {
        env_logger::Builder::from_env(
            env_logger::Env::default().default_filter_or("gear_core=debug"),
        )
        .format_module_path(false)
        .format_level(true)
        .try_init()
        .expect("cannot init logger");
        let mut data = PageBufInner::filled_with(199u8);
        data.inner_mut()[1] = 2;
        let page_buf = PageBuf::from_inner(data);
        log::debug!("page buff = {:?}", page_buf);
    }
    #[test]
    fn free_fails() {
        let mut ctx = AllocationsContext::new(BTreeSet::default(), WasmPage(0), WasmPage(0));
        assert_eq!(ctx.free(WasmPage(1)), Err(AllocError::InvalidFree(1)));
        let mut ctx = AllocationsContext::new(BTreeSet::default(), WasmPage(1), WasmPage(0));
        assert_eq!(ctx.free(WasmPage(0)), Err(AllocError::InvalidFree(0)));
        let mut ctx =
            AllocationsContext::new(BTreeSet::from([WasmPage(0)]), WasmPage(1), WasmPage(1));
        assert_eq!(ctx.free(WasmPage(1)), Err(AllocError::InvalidFree(1)));
        let mut ctx = AllocationsContext::new(
            BTreeSet::from([WasmPage(1), WasmPage(3)]),
            WasmPage(1),
            WasmPage(4),
        );
        assert_eq!(ctx.free_range(WasmPage(1)..=WasmPage(3)), Ok(()));
    }
    #[test]
    fn page_iterator() {
        let test = |num1, num2| {
            let p1 = GearPage::from(num1);
            let p2 = GearPage::from(num2);
            assert_eq!(
                p1.iter_end(p2).unwrap().collect::<Vec<GearPage>>(),
                (num1..num2).map(GearPage::from).collect::<Vec<GearPage>>(),
            );
            assert_eq!(
                p1.iter_end_inclusive(p2)
                    .unwrap()
                    .collect::<Vec<GearPage>>(),
                (num1..=num2).map(GearPage::from).collect::<Vec<GearPage>>(),
            );
            assert_eq!(
                p1.iter_count(p2).unwrap().collect::<Vec<GearPage>>(),
                (num1..num1 + num2)
                    .map(GearPage::from)
                    .collect::<Vec<GearPage>>(),
            );
            assert_eq!(
                p1.iter_from_zero().collect::<Vec<GearPage>>(),
                (0..num1).map(GearPage::from).collect::<Vec<GearPage>>(),
            );
            assert_eq!(
                p1.iter_from_zero_inclusive().collect::<Vec<GearPage>>(),
                (0..=num1).map(GearPage::from).collect::<Vec<GearPage>>(),
            );
        };
        test(0, 1);
        test(111, 365);
        test(1238, 3498);
        test(0, 64444);
    }
    mod property_tests {
        use super::*;
        use crate::{memory::HostPointer, pages::PageError};
        use proptest::{
            arbitrary::any,
            collection::size_range,
            prop_oneof, proptest,
            strategy::{Just, Strategy},
            test_runner::Config as ProptestConfig,
        };
        struct TestMemory(WasmPage);
        impl Memory for TestMemory {
            type GrowError = PageError;
            fn grow(&mut self, pages: WasmPage) -> Result<(), Self::GrowError> {
                self.0 = self.0.add(pages)?;
                Ok(())
            }
            fn size(&self) -> WasmPage {
                self.0
            }
            fn write(&mut self, _offset: u32, _buffer: &[u8]) -> Result<(), MemoryError> {
                unimplemented!()
            }
            fn read(&self, _offset: u32, _buffer: &mut [u8]) -> Result<(), MemoryError> {
                unimplemented!()
            }
            unsafe fn get_buffer_host_addr_unsafe(&mut self) -> HostPointer {
                unimplemented!()
            }
        }
        #[derive(Debug, Clone)]
        enum Action {
            Alloc { pages: WasmPage },
            Free { page: WasmPage },
            FreeRange { page: WasmPage, size: u8 },
        }
        fn actions() -> impl Strategy<Value = Vec<Action>> {
            let action = wasm_page_number().prop_flat_map(|page| {
                prop_oneof![
                    Just(Action::Alloc { pages: page }),
                    Just(Action::Free { page }),
                    any::<u8>().prop_map(move |size| Action::FreeRange { page, size }),
                ]
            });
            proptest::collection::vec(action, 0..1024)
        }
        fn allocations() -> impl Strategy<Value = BTreeSet<WasmPage>> {
            proptest::collection::btree_set(wasm_page_number(), size_range(0..1024))
        }
        fn wasm_page_number() -> impl Strategy<Value = WasmPage> {
            any::<u16>().prop_map(WasmPage::from)
        }
        fn proptest_config() -> ProptestConfig {
            ProptestConfig {
                cases: 1024,
                ..Default::default()
            }
        }
        #[track_caller]
        fn assert_alloc_error(err: AllocError) {
            match err {
                AllocError::IncorrectAllocationData(_) | AllocError::ProgramAllocOutOfBounds => {}
                err => panic!("{err:?}"),
            }
        }
        #[track_caller]
        fn assert_free_error(err: AllocError) {
            match err {
                AllocError::InvalidFree(_) => {}
                AllocError::InvalidFreeRange(_, _) => {}
                err => panic!("{err:?}"),
            }
        }
        proptest! {
            #![proptest_config(proptest_config())]
            #[test]
            fn alloc(
                static_pages in wasm_page_number(),
                allocations in allocations(),
                max_pages in wasm_page_number(),
                mem_size in wasm_page_number(),
                actions in actions(),
            ) {
                let _ = env_logger::try_init();
                let mut ctx = AllocationsContext::new(allocations, static_pages, max_pages);
                let mut mem = TestMemory(mem_size);
                for action in actions {
                    match action {
                        Action::Alloc { pages } => {
                            if let Err(err) = ctx.alloc::<NoopGrowHandler>(pages, &mut mem, |_| Ok(())) {
                                assert_alloc_error(err);
                            }
                        }
                        Action::Free { page } => {
                            if let Err(err) = ctx.free(page) {
                                assert_free_error(err);
                            }
                        }
                        Action::FreeRange { page, size } => {
                            let end = WasmPage::from(page.0.saturating_add(size as u32) as u16);
                            if let Err(err) = ctx.free_range(page..=end) {
                                assert_free_error(err);
                            }
                        }
                    }
                }
            }
        }
    }
}