//! The Ethereal IR function.

pub mod block;
pub mod queue_element;
pub mod r#type;
pub mod visited_element;

use std::collections::BTreeMap;
use std::collections::BTreeSet;
use std::collections::HashMap;

use inkwell::types::BasicType;
use inkwell::values::BasicValue;
use num::CheckedAdd;
use num::CheckedDiv;
use num::CheckedMul;
use num::CheckedSub;
use num::Num;
use num::One;
use num::ToPrimitive;
use num::Zero;

use crate::compiler::standard_json::output::contract::evm::extra_metadata::recursive_function::RecursiveFunction;
use crate::compiler::standard_json::output::contract::evm::extra_metadata::ExtraMetadata;
use crate::evmla::assembly::instruction::name::Name as InstructionName;
use crate::evmla::assembly::instruction::Instruction;
use crate::evmla::ethereal_ir::function::block::element::stack::element::Element;
use crate::evmla::ethereal_ir::function::block::element::stack::Stack;
use crate::evmla::ethereal_ir::EtherealIR;

use self::block::element::stack::element::Element as StackElement;
use self::block::element::Element as BlockElement;
use self::block::Block;
use self::queue_element::QueueElement;
use self::r#type::Type;
use self::visited_element::VisitedElement;

/// The Ethereal IR function.
#[derive(Debug, Clone)]
pub struct Function {
    /// The Solidity compiler version.
    pub solc_version: semver::Version,
    /// The function name.
    pub name: String,
    /// The separately labelled blocks.
    pub blocks: BTreeMap<revive_llvm_context::PolkaVMFunctionBlockKey, Vec<Block>>,
    /// The function type.
    pub r#type: Type,
    /// The function stack size.
    pub stack_size: usize,
}

impl Function {
    /// A shortcut constructor.
    pub fn new(solc_version: semver::Version, r#type: Type) -> Self {
        let name = match r#type {
            Type::Initial => EtherealIR::DEFAULT_ENTRY_FUNCTION_NAME.to_string(),
            Type::Recursive {
                ref name,
                ref block_key,
                ..
            } => format!("{name}_{block_key}"),
        };

        Self {
            solc_version,
            name,
            blocks: BTreeMap::new(),
            r#type,
            stack_size: 0,
        }
    }

    /// Runs the function block traversal.
    pub fn traverse(
        &mut self,
        blocks: &HashMap<revive_llvm_context::PolkaVMFunctionBlockKey, Block>,
        functions: &mut BTreeMap<revive_llvm_context::PolkaVMFunctionBlockKey, Self>,
        extra_metadata: &ExtraMetadata,
        visited_functions: &mut BTreeSet<VisitedElement>,
    ) -> anyhow::Result<()> {
        let mut visited_blocks = BTreeSet::new();

        match self.r#type {
            Type::Initial => {
                for code_type in [
                    revive_llvm_context::PolkaVMCodeType::Deploy,
                    revive_llvm_context::PolkaVMCodeType::Runtime,
                ] {
                    self.consume_block(
                        blocks,
                        functions,
                        extra_metadata,
                        visited_functions,
                        &mut visited_blocks,
                        QueueElement::new(
                            revive_llvm_context::PolkaVMFunctionBlockKey::new(
                                code_type,
                                num::BigUint::zero(),
                            ),
                            None,
                            Stack::new(),
                        ),
                    )?;
                }
            }
            Type::Recursive {
                ref block_key,
                input_size,
                output_size,
                ..
            } => {
                let mut stack = Stack::with_capacity(1 + input_size);
                stack.push(Element::ReturnAddress(output_size));
                stack.append(&mut Stack::new_with_elements(vec![
                    Element::value(
                        "ARGUMENT".to_owned()
                    );
                    input_size
                ]));

                self.consume_block(
                    blocks,
                    functions,
                    extra_metadata,
                    visited_functions,
                    &mut visited_blocks,
                    QueueElement::new(block_key.to_owned(), None, stack),
                )?;
            }
        }

        self.finalize();

        Ok(())
    }

    /// Consumes the entry or a conditional block attached to another one.
    fn consume_block(
        &mut self,
        blocks: &HashMap<revive_llvm_context::PolkaVMFunctionBlockKey, Block>,
        functions: &mut BTreeMap<revive_llvm_context::PolkaVMFunctionBlockKey, Self>,
        extra_metadata: &ExtraMetadata,
        visited_functions: &mut BTreeSet<VisitedElement>,
        visited_blocks: &mut BTreeSet<VisitedElement>,
        mut queue_element: QueueElement,
    ) -> anyhow::Result<()> {
        let version = self.solc_version.to_owned();

        let mut queue = vec![];

        let mut block = blocks
            .get(&queue_element.block_key)
            .cloned()
            .ok_or_else(|| {
                anyhow::anyhow!("Undeclared destination block {}", queue_element.block_key)
            })?;
        block.initial_stack = queue_element.stack.clone();
        let block = self.insert_block(block);
        block.stack = block.initial_stack.clone();
        if let Some(predecessor) = queue_element.predecessor.take() {
            block.insert_predecessor(predecessor.0, predecessor.1);
        }

        let visited_element =
            VisitedElement::new(queue_element.block_key.clone(), queue_element.stack.hash());
        if visited_blocks.contains(&visited_element) {
            return Ok(());
        }
        visited_blocks.insert(visited_element);

        let mut block_size = 0;
        for block_element in block.elements.iter_mut() {
            block_size += 1;

            if Self::handle_instruction(
                blocks,
                functions,
                extra_metadata,
                visited_functions,
                block.key.code_type,
                block.instance.unwrap_or_default(),
                &mut block.stack,
                block_element,
                &version,
                &mut queue,
                &mut queue_element,
            )
            .is_err()
            {
                block_element.instruction = Instruction::invalid(&block_element.instruction);
                block_element.stack = block.stack.clone();
                break;
            }
        }
        block.elements.truncate(block_size);

        for element in queue.into_iter() {
            self.consume_block(
                blocks,
                functions,
                extra_metadata,
                visited_functions,
                visited_blocks,
                element,
            )?;
        }

        Ok(())
    }

    /// Processes an instruction, returning an error, if there is an invalid stack state.
    /// The blocks with an invalid stack state are considered being partially unreachable, and
    /// the invalid part is truncated after terminating with an `INVALID` instruction.
    #[allow(clippy::too_many_arguments)]
    fn handle_instruction(
        blocks: &HashMap<revive_llvm_context::PolkaVMFunctionBlockKey, Block>,
        functions: &mut BTreeMap<revive_llvm_context::PolkaVMFunctionBlockKey, Self>,
        extra_metadata: &ExtraMetadata,
        visited_functions: &mut BTreeSet<VisitedElement>,
        code_type: revive_llvm_context::PolkaVMCodeType,
        instance: usize,
        block_stack: &mut Stack,
        block_element: &mut BlockElement,
        version: &semver::Version,
        queue: &mut Vec<QueueElement>,
        queue_element: &mut QueueElement,
    ) -> anyhow::Result<()> {
        let (stack_output, queue_element) = match block_element.instruction {
            Instruction {
                name: InstructionName::PUSH_Tag,
                value: Some(ref tag),
                ..
            } => {
                let tag: num::BigUint = tag.parse().expect("Always valid");
                (vec![Element::Tag(tag & num::BigUint::from(u64::MAX))], None)
            }
            ref instruction @ Instruction {
                name: InstructionName::JUMP,
                ..
            } => {
                queue_element.predecessor = Some((queue_element.block_key.clone(), instance));

                let block_key = match block_stack
                    .elements
                    .last()
                    .ok_or_else(|| anyhow::anyhow!("Destination tag is missing"))?
                {
                    Element::Tag(destination) if destination > &num::BigUint::from(u32::MAX) => {
                        revive_llvm_context::PolkaVMFunctionBlockKey::new(
                            revive_llvm_context::PolkaVMCodeType::Runtime,
                            destination.to_owned() - num::BigUint::from(1u64 << 32),
                        )
                    }
                    Element::Tag(destination) => revive_llvm_context::PolkaVMFunctionBlockKey::new(
                        code_type,
                        destination.to_owned(),
                    ),
                    Element::ReturnAddress(output_size) => {
                        block_element.instruction =
                            Instruction::recursive_return(1 + output_size, instruction);
                        Self::update_io_data(block_stack, block_element, 1 + output_size, vec![])?;
                        return Ok(());
                    }
                    element => {
                        return Err(anyhow::anyhow!(
                            "The {} instruction expected a tag or return address, found {}",
                            instruction.name,
                            element
                        ));
                    }
                };

                let (next_block_key, stack_output) =
                    if let Some(recursive_function) = extra_metadata.get(&block_key) {
                        Self::handle_recursive_function_call(
                            recursive_function,
                            blocks,
                            functions,
                            extra_metadata,
                            visited_functions,
                            block_key,
                            block_stack,
                            block_element,
                            version,
                        )?
                    } else {
                        (block_key, vec![])
                    };

                (
                    stack_output,
                    Some(QueueElement::new(
                        next_block_key,
                        queue_element.predecessor.clone(),
                        Stack::new(),
                    )),
                )
            }
            ref instruction @ Instruction {
                name: InstructionName::JUMPI,
                ..
            } => {
                queue_element.predecessor = Some((queue_element.block_key.clone(), instance));

                let block_key = match block_stack
                    .elements
                    .last()
                    .ok_or_else(|| anyhow::anyhow!("Destination tag is missing"))?
                {
                    Element::Tag(destination) if destination > &num::BigUint::from(u32::MAX) => {
                        revive_llvm_context::PolkaVMFunctionBlockKey::new(
                            revive_llvm_context::PolkaVMCodeType::Runtime,
                            destination.to_owned() - num::BigUint::from(1u64 << 32),
                        )
                    }
                    Element::Tag(destination) => revive_llvm_context::PolkaVMFunctionBlockKey::new(
                        code_type,
                        destination.to_owned(),
                    ),
                    element => {
                        return Err(anyhow::anyhow!(
                            "The {} instruction expected a tag or return address, found {}",
                            instruction.name,
                            element
                        ));
                    }
                };

                (
                    vec![],
                    Some(QueueElement::new(
                        block_key,
                        queue_element.predecessor.clone(),
                        Stack::new(),
                    )),
                )
            }
            Instruction {
                name: InstructionName::Tag,
                value: Some(ref tag),
                ..
            } => {
                let tag: num::BigUint = tag.parse().expect("Always valid");
                let block_key = revive_llvm_context::PolkaVMFunctionBlockKey::new(code_type, tag);

                queue_element.predecessor = Some((queue_element.block_key.clone(), instance));
                queue_element.block_key = block_key.clone();

                (
                    vec![],
                    Some(QueueElement::new(
                        block_key,
                        queue_element.predecessor.clone(),
                        Stack::new(),
                    )),
                )
            }

            Instruction {
                name: InstructionName::SWAP1,
                ..
            } => {
                block_stack.swap(1)?;
                (vec![], None)
            }
            Instruction {
                name: InstructionName::SWAP2,
                ..
            } => {
                block_stack.swap(2)?;
                (vec![], None)
            }
            Instruction {
                name: InstructionName::SWAP3,
                ..
            } => {
                block_stack.swap(3)?;
                (vec![], None)
            }
            Instruction {
                name: InstructionName::SWAP4,
                ..
            } => {
                block_stack.swap(4)?;
                (vec![], None)
            }
            Instruction {
                name: InstructionName::SWAP5,
                ..
            } => {
                block_stack.swap(5)?;
                (vec![], None)
            }
            Instruction {
                name: InstructionName::SWAP6,
                ..
            } => {
                block_stack.swap(6)?;
                (vec![], None)
            }
            Instruction {
                name: InstructionName::SWAP7,
                ..
            } => {
                block_stack.swap(7)?;
                (vec![], None)
            }
            Instruction {
                name: InstructionName::SWAP8,
                ..
            } => {
                block_stack.swap(8)?;
                (vec![], None)
            }
            Instruction {
                name: InstructionName::SWAP9,
                ..
            } => {
                block_stack.swap(9)?;
                (vec![], None)
            }
            Instruction {
                name: InstructionName::SWAP10,
                ..
            } => {
                block_stack.swap(10)?;
                (vec![], None)
            }
            Instruction {
                name: InstructionName::SWAP11,
                ..
            } => {
                block_stack.swap(11)?;
                (vec![], None)
            }
            Instruction {
                name: InstructionName::SWAP12,
                ..
            } => {
                block_stack.swap(12)?;
                (vec![], None)
            }
            Instruction {
                name: InstructionName::SWAP13,
                ..
            } => {
                block_stack.swap(13)?;
                (vec![], None)
            }
            Instruction {
                name: InstructionName::SWAP14,
                ..
            } => {
                block_stack.swap(14)?;
                (vec![], None)
            }
            Instruction {
                name: InstructionName::SWAP15,
                ..
            } => {
                block_stack.swap(15)?;
                (vec![], None)
            }
            Instruction {
                name: InstructionName::SWAP16,
                ..
            } => {
                block_stack.swap(16)?;
                (vec![], None)
            }

            Instruction {
                name: InstructionName::DUP1,
                ..
            } => (vec![block_stack.dup(1)?], None),
            Instruction {
                name: InstructionName::DUP2,
                ..
            } => (vec![block_stack.dup(2)?], None),
            Instruction {
                name: InstructionName::DUP3,
                ..
            } => (vec![block_stack.dup(3)?], None),
            Instruction {
                name: InstructionName::DUP4,
                ..
            } => (vec![block_stack.dup(4)?], None),
            Instruction {
                name: InstructionName::DUP5,
                ..
            } => (vec![block_stack.dup(5)?], None),
            Instruction {
                name: InstructionName::DUP6,
                ..
            } => (vec![block_stack.dup(6)?], None),
            Instruction {
                name: InstructionName::DUP7,
                ..
            } => (vec![block_stack.dup(7)?], None),
            Instruction {
                name: InstructionName::DUP8,
                ..
            } => (vec![block_stack.dup(8)?], None),
            Instruction {
                name: InstructionName::DUP9,
                ..
            } => (vec![block_stack.dup(9)?], None),
            Instruction {
                name: InstructionName::DUP10,
                ..
            } => (vec![block_stack.dup(10)?], None),
            Instruction {
                name: InstructionName::DUP11,
                ..
            } => (vec![block_stack.dup(11)?], None),
            Instruction {
                name: InstructionName::DUP12,
                ..
            } => (vec![block_stack.dup(12)?], None),
            Instruction {
                name: InstructionName::DUP13,
                ..
            } => (vec![block_stack.dup(13)?], None),
            Instruction {
                name: InstructionName::DUP14,
                ..
            } => (vec![block_stack.dup(14)?], None),
            Instruction {
                name: InstructionName::DUP15,
                ..
            } => (vec![block_stack.dup(15)?], None),
            Instruction {
                name: InstructionName::DUP16,
                ..
            } => (vec![block_stack.dup(16)?], None),

            Instruction {
                name:
                    InstructionName::PUSH
                    | InstructionName::PUSH1
                    | InstructionName::PUSH2
                    | InstructionName::PUSH3
                    | InstructionName::PUSH4
                    | InstructionName::PUSH5
                    | InstructionName::PUSH6
                    | InstructionName::PUSH7
                    | InstructionName::PUSH8
                    | InstructionName::PUSH9
                    | InstructionName::PUSH10
                    | InstructionName::PUSH11
                    | InstructionName::PUSH12
                    | InstructionName::PUSH13
                    | InstructionName::PUSH14
                    | InstructionName::PUSH15
                    | InstructionName::PUSH16
                    | InstructionName::PUSH17
                    | InstructionName::PUSH18
                    | InstructionName::PUSH19
                    | InstructionName::PUSH20
                    | InstructionName::PUSH21
                    | InstructionName::PUSH22
                    | InstructionName::PUSH23
                    | InstructionName::PUSH24
                    | InstructionName::PUSH25
                    | InstructionName::PUSH26
                    | InstructionName::PUSH27
                    | InstructionName::PUSH28
                    | InstructionName::PUSH29
                    | InstructionName::PUSH30
                    | InstructionName::PUSH31
                    | InstructionName::PUSH32,
                value: Some(ref constant),
                ..
            } => (
                vec![num::BigUint::from_str_radix(
                    constant.as_str(),
                    revive_common::BASE_HEXADECIMAL,
                )
                .map(StackElement::Constant)?],
                None,
            ),
            Instruction {
                name:
                    InstructionName::PUSH_ContractHash
                    | InstructionName::PUSH_ContractHashSize
                    | InstructionName::PUSHLIB,
                value: Some(ref path),
                ..
            } => (vec![StackElement::Path(path.to_owned())], None),
            Instruction {
                name: InstructionName::PUSH_Data,
                value: Some(ref data),
                ..
            } => (vec![StackElement::Data(data.to_owned())], None),
            ref instruction @ Instruction {
                name: InstructionName::PUSHDEPLOYADDRESS,
                ..
            } => (
                vec![StackElement::value(instruction.name.to_string())],
                None,
            ),

            ref instruction @ Instruction {
                name: InstructionName::ADD,
                ..
            } => {
                let operands = &block_stack.elements[block_stack.elements.len() - 2..];

                let result = match (&operands[1], &operands[0]) {
                    (Element::Tag(operand_1), Element::Constant(operand_2))
                    | (Element::Constant(operand_1), Element::Tag(operand_2))
                    | (Element::Tag(operand_1), Element::Tag(operand_2)) => {
                        match operand_1.checked_add(operand_2) {
                            Some(result) if Self::is_tag_value_valid(blocks, &result) => {
                                Element::Tag(result)
                            }
                            Some(_result) => Element::value(instruction.name.to_string()),
                            None => Element::value(instruction.name.to_string()),
                        }
                    }
                    (Element::Constant(operand_1), Element::Constant(operand_2)) => {
                        match operand_1.checked_add(operand_2) {
                            Some(result) => Element::Constant(result),
                            None => Element::value(instruction.name.to_string()),
                        }
                    }
                    _ => Element::value(instruction.name.to_string()),
                };

                (vec![result], None)
            }
            ref instruction @ Instruction {
                name: InstructionName::SUB,
                ..
            } => {
                let operands = &block_stack.elements[block_stack.elements.len() - 2..];

                let result = match (&operands[1], &operands[0]) {
                    (Element::Tag(operand_1), Element::Constant(operand_2))
                    | (Element::Constant(operand_1), Element::Tag(operand_2))
                    | (Element::Tag(operand_1), Element::Tag(operand_2)) => {
                        match operand_1.checked_sub(operand_2) {
                            Some(result) if Self::is_tag_value_valid(blocks, &result) => {
                                Element::Tag(result)
                            }
                            Some(_result) => Element::value(instruction.name.to_string()),
                            None => Element::value(instruction.name.to_string()),
                        }
                    }
                    (Element::Constant(operand_1), Element::Constant(operand_2)) => {
                        match operand_1.checked_sub(operand_2) {
                            Some(result) => Element::Constant(result),
                            None => Element::value(instruction.name.to_string()),
                        }
                    }
                    _ => Element::value(instruction.name.to_string()),
                };

                (vec![result], None)
            }
            ref instruction @ Instruction {
                name: InstructionName::MUL,
                ..
            } => {
                let operands = &block_stack.elements[block_stack.elements.len() - 2..];

                let result = match (&operands[1], &operands[0]) {
                    (Element::Tag(operand_1), Element::Constant(operand_2))
                    | (Element::Constant(operand_1), Element::Tag(operand_2))
                    | (Element::Tag(operand_1), Element::Tag(operand_2)) => {
                        match operand_1.checked_mul(operand_2) {
                            Some(result) if Self::is_tag_value_valid(blocks, &result) => {
                                Element::Tag(result)
                            }
                            Some(_result) => Element::value(instruction.name.to_string()),
                            None => Element::value(instruction.name.to_string()),
                        }
                    }
                    (Element::Constant(operand_1), Element::Constant(operand_2)) => {
                        match operand_1.checked_mul(operand_2) {
                            Some(result) => Element::Constant(result),
                            None => Element::value(instruction.name.to_string()),
                        }
                    }
                    _ => Element::value(instruction.name.to_string()),
                };

                (vec![result], None)
            }
            ref instruction @ Instruction {
                name: InstructionName::DIV,
                ..
            } => {
                let operands = &block_stack.elements[block_stack.elements.len() - 2..];

                let result = match (&operands[1], &operands[0]) {
                    (Element::Tag(operand_1), Element::Constant(operand_2))
                    | (Element::Constant(operand_1), Element::Tag(operand_2))
                    | (Element::Tag(operand_1), Element::Tag(operand_2)) => {
                        if operand_2.is_zero() {
                            Element::Tag(num::BigUint::zero())
                        } else {
                            match operand_1.checked_div(operand_2) {
                                Some(result) if Self::is_tag_value_valid(blocks, &result) => {
                                    Element::Tag(result)
                                }
                                Some(_result) => Element::value(instruction.name.to_string()),
                                None => Element::value(instruction.name.to_string()),
                            }
                        }
                    }
                    (Element::Constant(operand_1), Element::Constant(operand_2)) => {
                        if operand_2.is_zero() {
                            Element::Constant(num::BigUint::zero())
                        } else {
                            match operand_1.checked_div(operand_2) {
                                Some(result) => Element::Constant(result),
                                None => Element::value(instruction.name.to_string()),
                            }
                        }
                    }
                    _ => Element::value(instruction.name.to_string()),
                };

                (vec![result], None)
            }
            ref instruction @ Instruction {
                name: InstructionName::MOD,
                ..
            } => {
                let operands = &block_stack.elements[block_stack.elements.len() - 2..];

                let result = match (&operands[1], &operands[0]) {
                    (Element::Tag(operand_1), Element::Constant(operand_2))
                    | (Element::Constant(operand_1), Element::Tag(operand_2))
                    | (Element::Tag(operand_1), Element::Tag(operand_2)) => {
                        if operand_2.is_zero() {
                            Element::Tag(num::BigUint::zero())
                        } else {
                            let result = operand_1 % operand_2;
                            if Self::is_tag_value_valid(blocks, &result) {
                                Element::Tag(result)
                            } else {
                                Element::value(instruction.name.to_string())
                            }
                        }
                    }
                    (Element::Constant(operand_1), Element::Constant(operand_2)) => {
                        if operand_2.is_zero() {
                            Element::Constant(num::BigUint::zero())
                        } else {
                            Element::Constant(operand_1 % operand_2)
                        }
                    }
                    _ => Element::value(instruction.name.to_string()),
                };

                (vec![result], None)
            }
            ref instruction @ Instruction {
                name: InstructionName::SHL,
                ..
            } => {
                let operands = &block_stack.elements[block_stack.elements.len() - 2..];

                let result = match (&operands[0], &operands[1]) {
                    (Element::Tag(tag), Element::Constant(offset)) => {
                        let offset = offset % revive_common::BIT_LENGTH_WORD;
                        let offset = offset.to_u64().expect("Always valid");
                        let result = tag << offset;
                        if Self::is_tag_value_valid(blocks, &result) {
                            Element::Tag(result)
                        } else {
                            Element::value(instruction.name.to_string())
                        }
                    }
                    (Element::Constant(constant), Element::Constant(offset)) => {
                        let offset = offset % revive_common::BIT_LENGTH_WORD;
                        let offset = offset.to_u64().expect("Always valid");
                        Element::Constant(constant << offset)
                    }
                    _ => Element::value(instruction.name.to_string()),
                };

                (vec![result], None)
            }
            ref instruction @ Instruction {
                name: InstructionName::SHR,
                ..
            } => {
                let operands = &block_stack.elements[block_stack.elements.len() - 2..];

                let result = match (&operands[0], &operands[1]) {
                    (Element::Tag(tag), Element::Constant(offset)) => {
                        let offset = offset % revive_common::BIT_LENGTH_WORD;
                        let offset = offset.to_u64().expect("Always valid");
                        let result = tag >> offset;
                        if Self::is_tag_value_valid(blocks, &result) {
                            Element::Tag(result)
                        } else {
                            Element::value(instruction.name.to_string())
                        }
                    }
                    (Element::Constant(constant), Element::Constant(offset)) => {
                        let offset = offset % revive_common::BIT_LENGTH_WORD;
                        let offset = offset.to_u64().expect("Always valid");
                        Element::Constant(constant >> offset)
                    }
                    _ => Element::value(instruction.name.to_string()),
                };

                (vec![result], None)
            }
            ref instruction @ Instruction {
                name: InstructionName::OR,
                ..
            } => {
                let operands = &block_stack.elements[block_stack.elements.len() - 2..];

                let result = match (&operands[1], &operands[0]) {
                    (Element::Tag(operand_1), Element::Tag(operand_2))
                    | (Element::Tag(operand_1), Element::Constant(operand_2))
                    | (Element::Constant(operand_1), Element::Tag(operand_2)) => {
                        let result = operand_1 | operand_2;
                        if Self::is_tag_value_valid(blocks, &result) {
                            Element::Tag(result)
                        } else {
                            Element::value(instruction.name.to_string())
                        }
                    }
                    (Element::Constant(operand_1), Element::Constant(operand_2)) => {
                        Element::Constant(operand_1 | operand_2)
                    }
                    _ => Element::value(instruction.name.to_string()),
                };

                (vec![result], None)
            }
            ref instruction @ Instruction {
                name: InstructionName::XOR,
                ..
            } => {
                let operands = &block_stack.elements[block_stack.elements.len() - 2..];

                let result = match (&operands[1], &operands[0]) {
                    (Element::Tag(operand_1), Element::Tag(operand_2))
                    | (Element::Tag(operand_1), Element::Constant(operand_2))
                    | (Element::Constant(operand_1), Element::Tag(operand_2)) => {
                        let result = operand_1 ^ operand_2;
                        if Self::is_tag_value_valid(blocks, &result) {
                            Element::Tag(result)
                        } else {
                            Element::value(instruction.name.to_string())
                        }
                    }
                    (Element::Constant(operand_1), Element::Constant(operand_2)) => {
                        Element::Constant(operand_1 ^ operand_2)
                    }
                    _ => Element::value(instruction.name.to_string()),
                };

                (vec![result], None)
            }
            ref instruction @ Instruction {
                name: InstructionName::AND,
                ..
            } => {
                let operands = &block_stack.elements[block_stack.elements.len() - 2..];

                let result = match (&operands[1], &operands[0]) {
                    (Element::Tag(operand_1), Element::Tag(operand_2))
                    | (Element::Tag(operand_1), Element::Constant(operand_2))
                    | (Element::Constant(operand_1), Element::Tag(operand_2)) => {
                        let result = operand_1 & operand_2;
                        if Self::is_tag_value_valid(blocks, &result) {
                            Element::Tag(result)
                        } else {
                            Element::value(instruction.name.to_string())
                        }
                    }
                    (Element::Constant(operand_1), Element::Constant(operand_2)) => {
                        Element::Constant(operand_1 & operand_2)
                    }
                    _ => Element::value(instruction.name.to_string()),
                };

                (vec![result], None)
            }

            ref instruction @ Instruction {
                name: InstructionName::LT,
                ..
            } => {
                let operands = &block_stack.elements[block_stack.elements.len() - 2..];

                let result = match (&operands[1], &operands[0]) {
                    (Element::Tag(operand_1), Element::Tag(operand_2)) => {
                        Element::Constant(num::BigUint::from(u64::from(operand_1 < operand_2)))
                    }
                    _ => Element::value(instruction.name.to_string()),
                };

                (vec![result], None)
            }
            ref instruction @ Instruction {
                name: InstructionName::GT,
                ..
            } => {
                let operands = &block_stack.elements[block_stack.elements.len() - 2..];

                let result = match (&operands[1], &operands[0]) {
                    (Element::Tag(operand_1), Element::Tag(operand_2)) => {
                        Element::Constant(num::BigUint::from(u64::from(operand_1 > operand_2)))
                    }
                    _ => Element::value(instruction.name.to_string()),
                };

                (vec![result], None)
            }
            ref instruction @ Instruction {
                name: InstructionName::EQ,
                ..
            } => {
                let operands = &block_stack.elements[block_stack.elements.len() - 2..];

                let result = match (&operands[1], &operands[0]) {
                    (Element::Tag(operand_1), Element::Tag(operand_2)) => {
                        Element::Constant(num::BigUint::from(u64::from(operand_1 == operand_2)))
                    }
                    _ => Element::value(instruction.name.to_string()),
                };

                (vec![result], None)
            }
            ref instruction @ Instruction {
                name: InstructionName::ISZERO,
                ..
            } => {
                let operand = block_stack
                    .elements
                    .last()
                    .ok_or_else(|| anyhow::anyhow!("Operand is missing"))?;

                let result = match operand {
                    Element::Tag(operand) => Element::Constant(if operand.is_zero() {
                        num::BigUint::one()
                    } else {
                        num::BigUint::zero()
                    }),
                    _ => Element::value(instruction.name.to_string()),
                };

                (vec![result], None)
            }

            ref instruction => (
                vec![Element::value(instruction.name.to_string()); instruction.output_size()],
                None,
            ),
        };

        Self::update_io_data(
            block_stack,
            block_element,
            block_element.instruction.input_size(version),
            stack_output,
        )?;

        if let Some(mut queue_element) = queue_element {
            block_element.stack.clone_into(&mut queue_element.stack);
            queue.push(queue_element);
        }

        Ok(())
    }

    /// Updates the stack data with input and output data.
    fn update_io_data(
        block_stack: &mut Stack,
        block_element: &mut BlockElement,
        input_size: usize,
        output_data: Vec<Element>,
    ) -> anyhow::Result<()> {
        if block_stack.len() < input_size {
            anyhow::bail!("Stack underflow");
        }
        block_element.stack_input = Stack::new_with_elements(
            block_stack
                .elements
                .drain(block_stack.len() - input_size..)
                .collect(),
        );
        block_element.stack_output = Stack::new_with_elements(output_data);
        block_stack.append(&mut block_element.stack_output.clone());
        block_element.stack = block_stack.clone();
        Ok(())
    }

    /// Handles the recursive function call.
    #[allow(clippy::too_many_arguments)]
    fn handle_recursive_function_call(
        recursive_function: &RecursiveFunction,
        blocks: &HashMap<revive_llvm_context::PolkaVMFunctionBlockKey, Block>,
        functions: &mut BTreeMap<revive_llvm_context::PolkaVMFunctionBlockKey, Self>,
        extra_metadata: &ExtraMetadata,
        visited_functions: &mut BTreeSet<VisitedElement>,
        block_key: revive_llvm_context::PolkaVMFunctionBlockKey,
        block_stack: &mut Stack,
        block_element: &mut BlockElement,
        version: &semver::Version,
    ) -> anyhow::Result<(revive_llvm_context::PolkaVMFunctionBlockKey, Vec<Element>)> {
        let return_address_offset = block_stack.elements.len() - 2 - recursive_function.input_size;
        let input_arguments_offset = return_address_offset + 1;
        let callee_tag_offset = input_arguments_offset + recursive_function.input_size;

        let return_address = match block_stack.elements[return_address_offset] {
            Element::Tag(ref return_address) => revive_llvm_context::PolkaVMFunctionBlockKey::new(
                block_key.code_type,
                return_address.to_owned(),
            ),
            ref element => anyhow::bail!("Expected the function return address, found {}", element),
        };
        let mut stack = Stack::with_capacity(1 + recursive_function.input_size);
        stack.push(StackElement::ReturnAddress(
            1 + recursive_function.output_size,
        ));
        stack.append(&mut Stack::new_with_elements(
            block_stack.elements[input_arguments_offset..callee_tag_offset].to_owned(),
        ));
        let stack_hash = stack.hash();

        let visited_element = VisitedElement::new(block_key.clone(), stack_hash);
        if !visited_functions.contains(&visited_element) {
            let mut function = Self::new(
                version.to_owned(),
                Type::new_recursive(
                    recursive_function.name.to_owned(),
                    block_key.clone(),
                    recursive_function.input_size,
                    recursive_function.output_size,
                ),
            );
            visited_functions.insert(visited_element);
            function.traverse(blocks, functions, extra_metadata, visited_functions)?;
            functions.insert(block_key.clone(), function);
        }

        let stack_output =
            vec![Element::value("RETURN_VALUE".to_owned()); recursive_function.output_size];
        let mut return_stack = Stack::new_with_elements(
            block_stack.elements[..block_stack.len() - recursive_function.input_size - 2]
                .to_owned(),
        );
        return_stack.append(&mut Stack::new_with_elements(stack_output.clone()));
        let return_stack_hash = return_stack.hash();

        block_element.instruction = Instruction::recursive_call(
            recursive_function.name.to_owned(),
            block_key,
            return_stack_hash,
            recursive_function.input_size + 2,
            recursive_function.output_size,
            return_address.clone(),
            &block_element.instruction,
        );

        Ok((return_address, stack_output))
    }

    /// Pushes a block into the function.
    fn insert_block(&mut self, mut block: Block) -> &mut Block {
        let key = block.key.clone();

        if let Some(entry) = self.blocks.get_mut(&key) {
            if entry.iter().all(|existing_block| {
                existing_block.initial_stack.hash() != block.initial_stack.hash()
            }) {
                block.instance = Some(entry.len());
                entry.push(block);
            }
        } else {
            block.instance = Some(0);
            self.blocks.insert(block.key.clone(), vec![block]);
        }

        self.blocks
            .get_mut(&key)
            .expect("Always exists")
            .last_mut()
            .expect("Always exists")
    }

    /// Checks whether the tag value actually references an existing block.
    /// Checks both deploy and runtime code.
    fn is_tag_value_valid(
        blocks: &HashMap<revive_llvm_context::PolkaVMFunctionBlockKey, Block>,
        tag: &num::BigUint,
    ) -> bool {
        blocks.contains_key(&revive_llvm_context::PolkaVMFunctionBlockKey::new(
            revive_llvm_context::PolkaVMCodeType::Deploy,
            tag & num::BigUint::from(u32::MAX),
        )) || blocks.contains_key(&revive_llvm_context::PolkaVMFunctionBlockKey::new(
            revive_llvm_context::PolkaVMCodeType::Runtime,
            tag & num::BigUint::from(u32::MAX),
        ))
    }

    /// Finalizes the function data.
    fn finalize(&mut self) {
        for (_tag, blocks) in self.blocks.iter() {
            for block in blocks.iter() {
                for block_element in block.elements.iter() {
                    let total_length = block_element.stack.elements.len()
                        + block_element.stack_input.len()
                        + block_element.stack_output.len();
                    if total_length > self.stack_size {
                        self.stack_size = total_length;
                    }
                }
            }
        }
    }
}

impl<D> revive_llvm_context::PolkaVMWriteLLVM<D> for Function
where
    D: revive_llvm_context::PolkaVMDependency + Clone,
{
    fn declare(
        &mut self,
        context: &mut revive_llvm_context::PolkaVMContext<D>,
    ) -> anyhow::Result<()> {
        let (function_type, output_size) = match self.r#type {
            Type::Initial => {
                let output_size = 0;
                let r#type = context.function_type(
                    vec![context
                        .integer_type(revive_common::BIT_LENGTH_BOOLEAN)
                        .as_basic_type_enum()],
                    output_size,
                );
                (r#type, output_size)
            }
            Type::Recursive {
                input_size,
                output_size,
                ..
            } => {
                let r#type = context.function_type(
                    vec![
                        context
                            .integer_type(revive_common::BIT_LENGTH_WORD)
                            .as_basic_type_enum();
                        input_size
                    ],
                    output_size,
                );
                (r#type, output_size)
            }
        };
        let function = context.add_function(
            self.name.as_str(),
            function_type,
            output_size,
            Some(inkwell::module::Linkage::Private),
        )?;
        function
            .borrow_mut()
            .set_evmla_data(revive_llvm_context::PolkaVMFunctionEVMLAData::new(
                self.stack_size,
            ));

        Ok(())
    }

    fn into_llvm(self, context: &mut revive_llvm_context::PolkaVMContext<D>) -> anyhow::Result<()> {
        context.set_current_function(self.name.as_str())?;

        for (key, blocks) in self.blocks.iter() {
            for (index, block) in blocks.iter().enumerate() {
                let inner = context.append_basic_block(format!("block_{key}/{index}").as_str());
                let mut stack_hashes = vec![block.initial_stack.hash()];
                stack_hashes.extend_from_slice(block.extra_hashes.as_slice());
                let evmla_data =
                    revive_llvm_context::PolkaVMFunctionBlockEVMLAData::new(stack_hashes);
                let mut block = revive_llvm_context::PolkaVMFunctionBlock::new(inner);
                block.set_evmla_data(evmla_data);
                context
                    .current_function()
                    .borrow_mut()
                    .evmla_mut()
                    .insert_block(key.to_owned(), block);
            }
        }

        context.set_basic_block(context.current_function().borrow().entry_block());
        let mut stack_variables = Vec::with_capacity(self.stack_size);
        for stack_index in 0..self.stack_size {
            let pointer = context.build_alloca(
                context.word_type(),
                format!("stack_var_{stack_index:03}").as_str(),
            );
            let value = match self.r#type {
                Type::Recursive { input_size, .. }
                    if stack_index >= 1 && stack_index <= input_size =>
                {
                    context
                        .current_function()
                        .borrow()
                        .declaration()
                        .value
                        .get_nth_param((stack_index - 1) as u32)
                        .expect("Always valid")
                }
                _ => context.word_const(0).as_basic_value_enum(),
            };
            context.build_store(pointer, value)?;
            stack_variables.push(revive_llvm_context::PolkaVMArgument::new(
                pointer.value.as_basic_value_enum(),
            ));
        }
        context.evmla_mut().stack = stack_variables;

        match self.r#type {
            Type::Initial => {
                let is_deploy_code_flag = context
                    .current_function()
                    .borrow()
                    .get_nth_param(0)
                    .into_int_value();
                let deploy_code_block = context.current_function().borrow().evmla().find_block(
                    &revive_llvm_context::PolkaVMFunctionBlockKey::new(
                        revive_llvm_context::PolkaVMCodeType::Deploy,
                        num::BigUint::zero(),
                    ),
                    &Stack::default().hash(),
                )?;
                let runtime_code_block = context.current_function().borrow().evmla().find_block(
                    &revive_llvm_context::PolkaVMFunctionBlockKey::new(
                        revive_llvm_context::PolkaVMCodeType::Runtime,
                        num::BigUint::zero(),
                    ),
                    &Stack::default().hash(),
                )?;
                context.build_conditional_branch(
                    is_deploy_code_flag,
                    deploy_code_block.inner(),
                    runtime_code_block.inner(),
                )?;
            }
            Type::Recursive { ref block_key, .. } => {
                let initial_block = context
                    .current_function()
                    .borrow()
                    .evmla()
                    .blocks
                    .get(block_key)
                    .expect("Always exists")
                    .first()
                    .expect("Always exists")
                    .inner();
                context.build_unconditional_branch(initial_block);
            }
        }

        for (key, blocks) in self.blocks.into_iter() {
            for (llvm_block, ir_block) in context
                .current_function()
                .borrow()
                .evmla()
                .blocks
                .get(&key)
                .cloned()
                .ok_or_else(|| anyhow::anyhow!("Undeclared function block {}", key))?
                .into_iter()
                .map(|block| block.inner())
                .zip(blocks)
            {
                context.set_basic_block(llvm_block);
                ir_block.into_llvm(context)?;
            }
        }

        context.set_basic_block(context.current_function().borrow().return_block());
        match context.current_function().borrow().r#return() {
            revive_llvm_context::PolkaVMFunctionReturn::None => {
                context.build_return(None);
            }
            revive_llvm_context::PolkaVMFunctionReturn::Primitive { pointer } => {
                let return_value = context.build_load(pointer, "return_value")?;
                context.build_return(Some(&return_value));
            }
            revive_llvm_context::PolkaVMFunctionReturn::Compound { pointer, .. } => {
                let return_value = context.build_load(pointer, "return_value")?;
                context.build_return(Some(&return_value));
            }
        }

        Ok(())
    }
}

impl std::fmt::Display for Function {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        match self.r#type {
            Type::Initial => writeln!(f, "function {} {{", self.name),
            Type::Recursive {
                input_size,
                output_size,
                ..
            } => writeln!(
                f,
                "function {}({}) -> {} {{",
                self.name, input_size, output_size
            ),
        }?;
        writeln!(f, "    stack_usage: {}", self.stack_size)?;
        for (_key, blocks) in self.blocks.iter() {
            for block in blocks.iter() {
                write!(f, "{block}")?;
            }
        }
        writeln!(f, "}}")?;
        Ok(())
    }
}
