use super::{Executor, InstructionPtr};
use crate::{
    engine::{utils::unreachable_unchecked, ResumableOutOfFuelError},
    errors::MemoryError,
    ir::{
        index::{Data, Memory},
        Op,
        Slot,
    },
    store::{PrunedStore, StoreError, StoreInner},
    Error,
    TrapCode,
};

impl Executor<'_> {
    /// Returns the [`Op::MemoryIndex`] parameter for an [`Op`].
    fn fetch_memory_index(&self, offset: usize) -> Memory {
        let mut addr: InstructionPtr = self.ip;
        addr.add(offset);
        match *addr.get() {
            Op::MemoryIndex { index } => index,
            unexpected => {
                // Safety: Wasmi translation guarantees that [`Op::MemoryIndex`] exists.
                unsafe {
                    unreachable_unchecked!("expected `Op::MemoryIndex` but found: {unexpected:?}")
                }
            }
        }
    }

    /// Returns the [`Op::DataIndex`] parameter for an [`Op`].
    fn fetch_data_segment_index(&self, offset: usize) -> Data {
        let mut addr: InstructionPtr = self.ip;
        addr.add(offset);
        match *addr.get() {
            Op::DataIndex { index } => index,
            unexpected => {
                // Safety: Wasmi translation guarantees that [`Op::DataIndex`] exists.
                unsafe {
                    unreachable_unchecked!("expected `Op::DataIndex` but found: {unexpected:?}")
                }
            }
        }
    }

    /// Executes an [`Op::DataDrop`].
    pub fn execute_data_drop(&mut self, store: &mut StoreInner, segment_index: Data) {
        let segment = self.get_data_segment(segment_index);
        store.resolve_data_mut(&segment).drop_bytes();
        self.next_instr();
    }

    /// Executes an [`Op::MemorySize`].
    pub fn execute_memory_size(&mut self, store: &StoreInner, result: Slot, memory: Memory) {
        self.execute_memory_size_impl(store, result, memory);
        self.next_instr()
    }

    /// Underlying implementation of [`Op::MemorySize`].
    fn execute_memory_size_impl(&mut self, store: &StoreInner, result: Slot, memory: Memory) {
        let memory = self.get_memory(memory);
        let size = store.resolve_memory(&memory).size();
        self.set_stack_slot(result, size);
    }

    /// Executes an [`Op::MemoryGrow`].
    pub fn execute_memory_grow(
        &mut self,
        store: &mut PrunedStore,
        result: Slot,
        delta: Slot,
    ) -> Result<(), Error> {
        let delta: u64 = self.get_stack_slot_as(delta);
        let memory = self.fetch_memory_index(1);
        if delta == 0 {
            // Case: growing by 0 pages means there is nothing to do
            self.execute_memory_size_impl(store.inner(), result, memory);
            return self.try_next_instr_at(2);
        }
        let memory = self.get_memory(memory);
        let return_value = match store.grow_memory(&memory, delta) {
            Ok(return_value) => {
                // The `memory.grow` operation might have invalidated the cached
                // linear memory so we need to reset it in order for the cache to
                // reload in case it is used again.
                //
                // Safety: the instance has not changed thus calling this is valid.
                unsafe { self.cache.update_memory(store.inner_mut()) };
                return_value
            }
            Err(StoreError::External(
                MemoryError::OutOfBoundsGrowth | MemoryError::OutOfSystemMemory,
            )) => {
                let memory_ty = store.inner().resolve_memory(&memory).ty();
                match memory_ty.is_64() {
                    true => u64::MAX,
                    false => u64::from(u32::MAX),
                }
            }
            Err(StoreError::External(MemoryError::OutOfFuel { required_fuel })) => {
                return Err(Error::from(ResumableOutOfFuelError::new(required_fuel)))
            }
            Err(StoreError::External(MemoryError::ResourceLimiterDeniedAllocation)) => {
                return Err(Error::from(TrapCode::GrowthOperationLimited))
            }
            Err(error) => {
                panic!("`table.grow`: internal interpreter error: {error}")
            }
        };
        self.set_stack_slot(result, return_value);
        self.try_next_instr_at(2)
    }

    /// Executes an [`Op::MemoryCopy`].
    pub fn execute_memory_copy(
        &mut self,
        store: &mut StoreInner,
        dst: Slot,
        src: Slot,
        len: Slot,
    ) -> Result<(), Error> {
        let dst: u64 = self.get_stack_slot_as(dst);
        let src: u64 = self.get_stack_slot_as(src);
        let len: u64 = self.get_stack_slot_as(len);
        let Ok(dst_index) = usize::try_from(dst) else {
            return Err(Error::from(TrapCode::MemoryOutOfBounds));
        };
        let Ok(src_index) = usize::try_from(src) else {
            return Err(Error::from(TrapCode::MemoryOutOfBounds));
        };
        let Ok(len) = usize::try_from(len) else {
            return Err(Error::from(TrapCode::MemoryOutOfBounds));
        };
        let dst_memory = self.fetch_memory_index(1);
        let src_memory = self.fetch_memory_index(2);
        if src_memory == dst_memory {
            return self
                .execute_memory_copy_within_impl(store, src_memory, dst_index, src_index, len);
        }
        let (src_memory, dst_memory, fuel) = store.resolve_memory_pair_and_fuel(
            &self.get_memory(src_memory),
            &self.get_memory(dst_memory),
        );
        // These accesses just perform the bounds checks required by the Wasm spec.
        let src_bytes = src_memory
            .data()
            .get(src_index..)
            .and_then(|memory| memory.get(..len))
            .ok_or(TrapCode::MemoryOutOfBounds)?;
        let dst_bytes = dst_memory
            .data_mut()
            .get_mut(dst_index..)
            .and_then(|memory| memory.get_mut(..len))
            .ok_or(TrapCode::MemoryOutOfBounds)?;
        fuel.consume_fuel_if(|costs| costs.fuel_for_copying_bytes(len as u64))?;
        dst_bytes.copy_from_slice(src_bytes);
        self.try_next_instr_at(3)
    }

    /// Executes a generic `memory.copy` instruction.
    fn execute_memory_copy_within_impl(
        &mut self,
        store: &mut StoreInner,
        memory: Memory,
        dst_index: usize,
        src_index: usize,
        len: usize,
    ) -> Result<(), Error> {
        let memory = self.get_memory(memory);
        let (memory, fuel) = store.resolve_memory_and_fuel_mut(&memory);
        let bytes = memory.data_mut();
        // These accesses just perform the bounds checks required by the Wasm spec.
        bytes
            .get(src_index..)
            .and_then(|memory| memory.get(..len))
            .ok_or(TrapCode::MemoryOutOfBounds)?;
        bytes
            .get(dst_index..)
            .and_then(|memory| memory.get(..len))
            .ok_or(TrapCode::MemoryOutOfBounds)?;
        fuel.consume_fuel_if(|costs| costs.fuel_for_copying_bytes(len as u64))?;
        bytes.copy_within(src_index..src_index.wrapping_add(len), dst_index);
        self.try_next_instr_at(3)
    }

    /// Executes an [`Op::MemoryFill`].
    pub fn execute_memory_fill(
        &mut self,
        store: &mut StoreInner,
        dst: Slot,
        value: Slot,
        len: Slot,
    ) -> Result<(), Error> {
        let dst: u64 = self.get_stack_slot_as(dst);
        let value: u8 = self.get_stack_slot_as(value);
        let len: u64 = self.get_stack_slot_as(len);
        self.execute_memory_fill_impl(store, dst, value, len)
    }

    /// Executes an [`Op::MemoryFillImm`].
    pub fn execute_memory_fill_imm(
        &mut self,
        store: &mut StoreInner,
        dst: Slot,
        value: u8,
        len: Slot,
    ) -> Result<(), Error> {
        let dst: u64 = self.get_stack_slot_as(dst);
        let len: u64 = self.get_stack_slot_as(len);
        self.execute_memory_fill_impl(store, dst, value, len)
    }

    /// Executes a generic `memory.fill` instruction.
    #[inline(never)]
    fn execute_memory_fill_impl(
        &mut self,
        store: &mut StoreInner,
        dst: u64,
        value: u8,
        len: u64,
    ) -> Result<(), Error> {
        let Ok(dst) = usize::try_from(dst) else {
            return Err(Error::from(TrapCode::MemoryOutOfBounds));
        };
        let Ok(len) = usize::try_from(len) else {
            return Err(Error::from(TrapCode::MemoryOutOfBounds));
        };
        let memory = self.fetch_memory_index(1);
        let memory = self.get_memory(memory);
        let (memory, fuel) = store.resolve_memory_and_fuel_mut(&memory);
        let slice = memory
            .data_mut()
            .get_mut(dst..)
            .and_then(|memory| memory.get_mut(..len))
            .ok_or(TrapCode::MemoryOutOfBounds)?;
        fuel.consume_fuel_if(|costs| costs.fuel_for_copying_bytes(len as u64))?;
        slice.fill(value);
        self.try_next_instr_at(2)
    }

    /// Executes an [`Op::MemoryInit`].
    pub fn execute_memory_init(
        &mut self,
        store: &mut StoreInner,
        dst: Slot,
        src: Slot,
        len: Slot,
    ) -> Result<(), Error> {
        let dst: u64 = self.get_stack_slot_as(dst);
        let src: u32 = self.get_stack_slot_as(src);
        let len: u32 = self.get_stack_slot_as(len);
        let Ok(dst_index) = usize::try_from(dst) else {
            return Err(Error::from(TrapCode::MemoryOutOfBounds));
        };
        let Ok(src_index) = usize::try_from(src) else {
            return Err(Error::from(TrapCode::MemoryOutOfBounds));
        };
        let Ok(len) = usize::try_from(len) else {
            return Err(Error::from(TrapCode::MemoryOutOfBounds));
        };
        let memory_index: Memory = self.fetch_memory_index(1);
        let data_index: Data = self.fetch_data_segment_index(2);
        let (memory, data, fuel) = store.resolve_memory_init_params(
            &self.get_memory(memory_index),
            &self.get_data_segment(data_index),
        );
        let memory = memory
            .data_mut()
            .get_mut(dst_index..)
            .and_then(|memory| memory.get_mut(..len))
            .ok_or(TrapCode::MemoryOutOfBounds)?;
        let data = data
            .bytes()
            .get(src_index..)
            .and_then(|data| data.get(..len))
            .ok_or(TrapCode::MemoryOutOfBounds)?;
        fuel.consume_fuel_if(|costs| costs.fuel_for_copying_bytes(len as u64))?;
        memory.copy_from_slice(data);
        self.try_next_instr_at(3)
    }
}
