use scale_info::TypeInfo;
use codec::{Encode, Decode};
use sp_runtime::{
    Perbill, RuntimeDebug, traits::UniqueSaturatedInto,
    helpers_128bit::multiply_by_rational_with_rounding,
};

use crate::{
    EvmAddress, EvmBytes32, ExodusSession, RoastSession,
    MAX_MESSAGE_SIZE,
};

pub trait EvmAbiEncode {
    fn size(&self) -> usize;
    fn abi_encode(&self, buffer: &mut [u8], cursor: &mut usize) -> Option<()>;
}

macro_rules! impl_evm_encodable {
    (numeric: $( $ty:ty ),* ) => {
        $(
            impl EvmAbiEncode for $ty {
                #[inline(always)]
                fn size(&self) -> usize {
                    core::mem::size_of::<Self>()
                }

                #[inline(always)]
                fn abi_encode(&self, buffer: &mut [u8], cursor: &mut usize) -> Option<()> {
                    let val_len = self.size();
                    let padding = 32 - val_len;

                    let padding_slice = buffer.get_mut(*cursor..*cursor + padding)?;
                    padding_slice.fill(0);
                    *cursor += padding;

                    let value_slice = buffer.get_mut(*cursor..*cursor + val_len)?;
                    value_slice.copy_from_slice(&self.to_be_bytes());
                    *cursor += val_len;

                    Some(())
                }
            }
        )*
    };

    (bytes_fixed: $( $ty:ty ),* ) => {
        $(
            impl EvmAbiEncode for $ty {
                #[inline(always)]
                fn size(&self) -> usize {
                    <Self as AsRef<[u8]>>::as_ref(self).len()
                }

                #[inline(always)]
                fn abi_encode(&self, buffer: &mut [u8], cursor: &mut usize) -> Option<()> {
                    if self.size() > 32 { return None; }

                    let val_len = self.size();
                    let padding = 32 - val_len;

                    let padding_slice = buffer.get_mut(*cursor..*cursor + padding)?;
                    padding_slice.fill(0);
                    *cursor += padding;

                    let value_slice = buffer.get_mut(*cursor..*cursor + val_len)?;
                    value_slice.copy_from_slice(self.as_ref());
                    *cursor += val_len;

                    Some(())
                }
            }
        )*
    };
}

impl_evm_encodable!(numeric: u8, u16, u32, u64, u128);
impl_evm_encodable!(bytes_fixed: EvmAddress, EvmBytes32);

macro_rules! abi_encode_evm_message {
    ($buffer:expr, $sig:expr, [ $( $val:expr ),* $(,)? ]) => {{
        let mut cursor = 0;
        let mut no_overflow = true;

        let hash = sp_io::hashing::keccak_256($sig.as_bytes());

        if let Some(selector_slice) = $buffer.get_mut(0..4) {
            selector_slice.copy_from_slice(&hash[0..4]);
            cursor += 4;

            $(
                if no_overflow {
                    if EvmAbiEncode::abi_encode($val, $buffer, &mut cursor).is_none() {
                        no_overflow = false;
                    }
                }
            )*
        } else {
            no_overflow = false;
        }

        no_overflow.then(|| &$buffer[0..cursor])
    }};
}

#[derive(Encode, Decode, Clone, PartialEq, TypeInfo, RuntimeDebug)]
pub struct ExodusRequest<NetworkId, Balance> {
    pub r#type: ExodusRequestType<NetworkId, Balance>,
	pub next_roast_session: RoastSession,
}

#[derive(Encode, Decode, Clone, PartialEq, TypeInfo, RuntimeDebug)]
pub enum ExodusRequestType<NetworkId, Balance> {
    EvmRotation {
        exodus_session: ExodusSession,
        public_key: EvmBytes32,
        parity: u8,
    },
    UtxoRotation,
    EvmBridgeOut {
        exodus_session: ExodusSession,
        network_id: NetworkId,
        amount: Balance,
        bounty: Perbill,
        receiver: EvmAddress,
    },
    EvmGovernance { network_id: NetworkId },
    UtxoBridgeOut { network_id: NetworkId },
}

impl<NetworkId, Balance> ExodusRequestType<NetworkId, Balance>
where
	NetworkId: UniqueSaturatedInto<u64> + Copy,
	Balance: UniqueSaturatedInto<u128> + Copy,
{
    pub fn get_message<'a>(
        &self,
        message_buffer: &'a mut [u8; MAX_MESSAGE_SIZE as usize],
    ) -> Option<&'a [u8]> {
        match &self {
            ExodusRequestType::EvmRotation { exodus_session, public_key, parity } => {
                abi_encode_evm_message!(
                    message_buffer,
                    "rotate(uint256,bytes32,uint8)",
                    [exodus_session, public_key, parity]
                )
            },
            ExodusRequestType::EvmBridgeOut { exodus_session, network_id, amount, bounty, receiver } => {
                let mut buffer = [0u8; 32];
                buffer[0..20].copy_from_slice(receiver.as_ref());

                let chain_id_u64: u64 = (*network_id).unique_saturated_into();
                buffer[20..28].copy_from_slice(&chain_id_u64.to_be_bytes());

                let scaled_bounty = multiply_by_rational_with_rounding(
                    bounty.deconstruct() as u128,
                    1u128 << 32,
                    1_000_000_000,
                    sp_runtime::Rounding::Down
                )?;

                let bounty_uint32 = scaled_bounty as u32;
                buffer[28..32].copy_from_slice(&bounty_uint32.to_be_bytes());

                let mut amount_buffer = [0u8; 32];
                let amount_u128: u128 = (*amount).unique_saturated_into();
                amount_buffer[16..32].copy_from_slice(&amount_u128.to_be_bytes());

                let packed_metadata = EvmBytes32::from(buffer);
                let amount_u256 = EvmBytes32::from(amount_buffer);

                abi_encode_evm_message!(
                    message_buffer,
                    "materialize(uint256,uint256,uint256)",
                    [exodus_session, &amount_u256, &packed_metadata]
                )
            }
            _ => None
        }
    }
}

impl<NetworkId, Balance> ExodusRequest<NetworkId, Balance>
where
	NetworkId: UniqueSaturatedInto<u64> + Copy,
	Balance: UniqueSaturatedInto<u128> + Copy,
{
    pub fn get_message<'a>(
        &self,
        message_buffer: &'a mut [u8; MAX_MESSAGE_SIZE as usize],
    ) -> Option<&'a [u8]> {
        self.r#type.get_message(message_buffer)
    }

    pub fn evm_rotation(
        exodus_session: ExodusSession,
        public_key: EvmBytes32,
        parity: u8,
    ) -> Self {
        let rotation = ExodusRequestType::EvmRotation {
            exodus_session,
            public_key,
            parity,
        };
        Self::default_request_for(rotation)
    }

    pub fn evm_bridge_out(
        exodus_session: ExodusSession,
        network_id: NetworkId,
        amount: Balance,
        bounty: Perbill,
        receiver: EvmAddress,
    ) -> Self {
        let evm_bridge_out = ExodusRequestType::EvmBridgeOut {
            exodus_session,
            network_id,
            amount,
            bounty,
            receiver,
        };
        Self::default_request_for(evm_bridge_out)
    }

    pub fn is_rotation(&self) -> bool {
        match &self.r#type {
            ExodusRequestType::EvmRotation { .. } | ExodusRequestType::UtxoRotation { .. } => true,
            _ => false,
        }
    }

    fn default_request_for(
        request_type: ExodusRequestType<NetworkId, Balance>,
    ) -> Self {
        Self { r#type: request_type, next_roast_session: Default::default() }
    }
}
