//! The entry function.

use inkwell::types::BasicType;
use revive_common::BIT_LENGTH_ETH_ADDRESS;
use revive_runtime_api::immutable_data::{
    GLOBAL_IMMUTABLE_DATA_POINTER, GLOBAL_IMMUTABLE_DATA_SIZE,
};
use revive_runtime_api::polkavm_imports::CALL_DATA_SIZE;
use revive_solc_json_interface::PolkaVMDefaultHeapMemorySize;

use crate::polkavm::context::address_space::AddressSpace;
use crate::polkavm::context::function::runtime;
use crate::polkavm::context::Context;
use crate::polkavm::WriteLLVM;

/// The entry function.
/// The function is a wrapper managing the runtime and deploy code calling logic.
/// Is a special runtime function that is only used by the front-end generated code.
#[derive(Debug, Default)]
pub struct Entry {}

impl Entry {
    /// The call flags argument index.
    pub const ARGUMENT_INDEX_CALL_FLAGS: usize = 0;

    /// Initializes the global variables.
    /// The pointers are not initialized, because it's not possible to create a null pointer.
    pub fn initialize_globals(context: &mut Context) -> anyhow::Result<()> {
        context.set_global(
            crate::polkavm::GLOBAL_CALLDATA_SIZE,
            context.xlen_type(),
            AddressSpace::Stack,
            context.xlen_type().get_undef(),
        );

        context.set_global(
            crate::polkavm::GLOBAL_HEAP_SIZE,
            context.xlen_type(),
            AddressSpace::Stack,
            context.xlen_type().const_zero(),
        );

        let heap_memory_type = context.byte_type().array_type(
            context
                .memory_config
                .heap_size
                .unwrap_or(PolkaVMDefaultHeapMemorySize),
        );
        context.set_global(
            crate::polkavm::GLOBAL_HEAP_MEMORY,
            heap_memory_type,
            AddressSpace::Stack,
            heap_memory_type.const_zero(),
        );

        let address_type = context.integer_type(BIT_LENGTH_ETH_ADDRESS);
        context.set_global(
            crate::polkavm::GLOBAL_ADDRESS_SPILL_BUFFER,
            address_type,
            AddressSpace::Stack,
            address_type.const_zero(),
        );

        Ok(())
    }

    /// Populate the calldata size global value.
    pub fn load_calldata_size(context: &mut Context) -> anyhow::Result<()> {
        let call_data_size_pointer = context
            .get_global(crate::polkavm::GLOBAL_CALLDATA_SIZE)?
            .value
            .as_pointer_value();
        let call_data_size_value = context
            .build_runtime_call(CALL_DATA_SIZE, &[])
            .expect("the call_data_size syscall method should return a value")
            .into_int_value();
        let call_data_size_value = context.builder().build_int_truncate(
            call_data_size_value,
            context.xlen_type(),
            "call_data_size_truncated",
        )?;
        context
            .builder()
            .build_store(call_data_size_pointer, call_data_size_value)?;

        Ok(())
    }

    /// Calls the deploy code if the first function argument was `1`.
    /// Calls the runtime code otherwise.
    pub fn leave_entry(context: &mut Context) -> anyhow::Result<()> {
        context.set_debug_location(0, 0, None)?;

        let is_deploy = context
            .current_function()
            .borrow()
            .get_nth_param(Self::ARGUMENT_INDEX_CALL_FLAGS);

        let deploy_code_call_block = context.append_basic_block("deploy_code_call_block");
        let runtime_code_call_block = context.append_basic_block("runtime_code_call_block");

        context.build_conditional_branch(
            is_deploy.into_int_value(),
            deploy_code_call_block,
            runtime_code_call_block,
        )?;

        let deploy_code = context
            .functions
            .get(runtime::FUNCTION_DEPLOY_CODE)
            .cloned()
            .ok_or_else(|| anyhow::anyhow!("Contract deploy code not found"))?;
        let runtime_code = context
            .functions
            .get(runtime::FUNCTION_RUNTIME_CODE)
            .cloned()
            .ok_or_else(|| anyhow::anyhow!("Contract runtime code not found"))?;

        context.set_basic_block(deploy_code_call_block);
        context.build_call(deploy_code.borrow().declaration, &[], "deploy_code_call");
        context.build_unconditional_branch(context.current_function().borrow().return_block());

        context.set_basic_block(runtime_code_call_block);
        context.build_call(runtime_code.borrow().declaration, &[], "runtime_code_call");
        context.build_unconditional_branch(context.current_function().borrow().return_block());

        Ok(())
    }
}

impl WriteLLVM for Entry {
    fn declare(&mut self, context: &mut Context) -> anyhow::Result<()> {
        let entry_arguments = vec![context.bool_type().as_basic_type_enum()];
        let entry_function_type = context.function_type(entry_arguments, 0);
        context.add_function(
            runtime::FUNCTION_ENTRY,
            entry_function_type,
            0,
            Some(inkwell::module::Linkage::External),
            None,
            false,
        )?;

        context.declare_global(
            GLOBAL_IMMUTABLE_DATA_POINTER,
            context.word_type().array_type(0),
            AddressSpace::Stack,
        );

        context.declare_global(
            GLOBAL_IMMUTABLE_DATA_SIZE,
            context.xlen_type(),
            AddressSpace::Stack,
        );

        Ok(())
    }

    /// Instead of a single entrypoint, the runtime expects two exports: `call ` and `deploy`.
    /// `call` and `deploy` directly call `entry`, signaling a deploy if the first arg is `1`.
    /// The `entry` function loads calldata, sets globals and calls the runtime or deploy code.
    fn into_llvm(self, context: &mut Context) -> anyhow::Result<()> {
        let entry = context
            .get_function(runtime::FUNCTION_ENTRY, false)
            .expect("the entry function should already be declared")
            .borrow()
            .declaration;
        crate::PolkaVMFunction::set_attributes(
            context.llvm(),
            entry,
            &[crate::PolkaVMAttribute::NoReturn],
            true,
        );

        context.set_current_function(runtime::FUNCTION_ENTRY, None, false)?;
        context.set_basic_block(context.current_function().borrow().entry_block());

        Self::initialize_globals(context)?;
        Self::load_calldata_size(context)?;
        Self::leave_entry(context)?;

        context.build_unconditional_branch(context.current_function().borrow().return_block());
        context.set_basic_block(context.current_function().borrow().return_block());
        context.build_unreachable();

        context.pop_debug_scope();

        Ok(())
    }
}
