use crate::call::{ContractCall, ContractError};
use ethers_core::{
    abi::{Detokenize, Function, Token, Tokenizable},
    types::{
        transaction::eip2718::TypedTransaction, Address, BlockNumber, Bytes, NameOrAddress, U256,
    },
};
use ethers_providers::{Middleware, PendingTransaction};
use std::{convert::TryFrom, fmt, result::Result as StdResult, sync::Arc};

/// The Multicall contract bindings. Auto-generated with `abigen`.
pub mod contract;
pub use contract::Multicall3 as MulticallContract;
use contract::{
    Call as Multicall1Call, Call3 as Multicall3Call, Call3Value as Multicall3CallValue,
    Result as MulticallResult,
};

pub mod constants;

/// Type alias for `Result<T, MulticallError<M>>`
pub type Result<T, M> = StdResult<T, error::MulticallError<M>>;

/// MultiCall error type
pub mod error;

/// Helper struct for managing calls to be made to the `function` in smart contract `target`
/// with `data`.
#[derive(Clone, Debug)]
pub struct Call {
    target: Address,
    data: Bytes,
    value: U256,
    allow_failure: bool,
    function: Function,
}

/// The version of the [`Multicall`](super::Multicall).
/// Used to determine which methods of the Multicall smart contract to use:
/// - [`Multicall`] : `aggregate((address,bytes)[])`
/// - [`Multicall2`] : `try_aggregate(bool, (address,bytes)[])`
/// - [`Multicall3`] : `aggregate3((address,bool,bytes)[])` or
///   `aggregate3Value((address,bool,uint256,bytes)[])`
///
/// [`Multicall`]: #variant.Multicall
/// [`Multicall2`]: #variant.Multicall2
/// [`Multicall3`]: #variant.Multicall3
#[repr(u8)]
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, PartialOrd, Ord)]
pub enum MulticallVersion {
    /// V1
    Multicall = 1,
    /// V2
    Multicall2 = 2,
    /// V3
    #[default]
    Multicall3 = 3,
}

impl From<MulticallVersion> for u8 {
    fn from(v: MulticallVersion) -> Self {
        v as u8
    }
}

impl TryFrom<u8> for MulticallVersion {
    type Error = String;
    fn try_from(v: u8) -> StdResult<Self, Self::Error> {
        match v {
            1 => Ok(MulticallVersion::Multicall),
            2 => Ok(MulticallVersion::Multicall2),
            3 => Ok(MulticallVersion::Multicall3),
            _ => Err(format!("Invalid Multicall version: {v}. Accepted values: 1, 2, 3.")),
        }
    }
}

impl MulticallVersion {
    /// True if call is v1
    #[inline]
    pub fn is_v1(&self) -> bool {
        matches!(self, Self::Multicall)
    }

    /// True if call is v2
    #[inline]
    pub fn is_v2(&self) -> bool {
        matches!(self, Self::Multicall2)
    }

    /// True if call is v3
    #[inline]
    pub fn is_v3(&self) -> bool {
        matches!(self, Self::Multicall3)
    }
}

/// A Multicall is an abstraction for sending batched calls/transactions to the Ethereum blockchain.
/// It stores an instance of the [`Multicall` smart contract](https://etherscan.io/address/0xcA11bde05977b3631167028862bE2a173976CA11#code)
/// and the user provided list of transactions to be called or executed on chain.
///
/// `Multicall` can be instantiated asynchronously from the chain ID of the provided client using
/// [`new`] or synchronously by providing a chain ID in [`new_with_chain`]. This, by default, uses
/// [`constants::MULTICALL_ADDRESS`], but can be overridden by providing `Some(address)`.
/// A list of all the supported chains is available [`here`](https://github.com/mds1/multicall#multicall3-contract-addresses).
///
/// Set the contract's version by using [`version`].
///
/// The `block` number can be provided for the call by using [`block`].
///
/// Transactions default to `EIP1559`. This can be changed by using [`legacy`].
///
/// Build on the `Multicall` instance by adding calls using [`add_call`] and call or broadcast them
/// all at once by using [`call`] and [`send`] respectively.
///
/// # Example
///
/// Using Multicall (version 1):
///
/// ```no_run
/// use ethers_core::{
///     abi::Abi,
///     types::{Address, H256, U256},
/// };
/// use ethers_contract::{Contract, Multicall, MulticallVersion};
/// use ethers_providers::{Middleware, Http, Provider, PendingTransaction};
/// use std::{convert::TryFrom, sync::Arc};
///
/// # async fn bar() -> Result<(), Box<dyn std::error::Error>> {
/// // this is a dummy address used for illustration purposes
/// let address = "eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee".parse::<Address>()?;
///
/// // (ugly way to write the ABI inline, you can otherwise read it from a file)
/// let abi: Abi = serde_json::from_str(r#"[{"inputs":[{"internalType":"string","name":"value","type":"string"}],"stateMutability":"nonpayable","type":"constructor"},{"anonymous":false,"inputs":[{"indexed":true,"internalType":"address","name":"author","type":"address"},{"indexed":true,"internalType":"address","name":"oldAuthor","type":"address"},{"indexed":false,"internalType":"string","name":"oldValue","type":"string"},{"indexed":false,"internalType":"string","name":"newValue","type":"string"}],"name":"ValueChanged","type":"event"},{"inputs":[],"name":"getValue","outputs":[{"internalType":"string","name":"","type":"string"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"lastSender","outputs":[{"internalType":"address","name":"","type":"address"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"string","name":"value","type":"string"}],"name":"setValue","outputs":[],"stateMutability":"nonpayable","type":"function"}]"#)?;
///
/// // connect to the network
/// let client = Provider::<Http>::try_from("http://localhost:8545")?;
///
/// // create the contract object. This will be used to construct the calls for multicall
/// let client = Arc::new(client);
/// let contract = Contract::<Provider<Http>>::new(address, abi, client.clone());
///
/// // note that these [`ContractCall`]s are futures, and need to be `.await`ed to resolve.
/// // But we will let `Multicall` to take care of that for us
/// let first_call = contract.method::<_, String>("getValue", ())?;
/// let second_call = contract.method::<_, Address>("lastSender", ())?;
///
/// // Since this example connects to a known chain, we need not provide an address for
/// // the Multicall contract and we set that to `None`. If you wish to provide the address
/// // for the Multicall contract, you can pass the `Some(multicall_addr)` argument.
/// // Construction of the `Multicall` instance follows the builder pattern:
/// let mut multicall = Multicall::new(client.clone(), None).await?;
/// multicall
///     .add_call(first_call, false)
///     .add_call(second_call, false);
///
/// // `await`ing on the `call` method lets us fetch the return values of both the above calls
/// // in one single RPC call
/// let return_data: (String, Address) = multicall.call().await?;
///
/// // the same `Multicall` instance can be re-used to do a different batch of transactions.
/// // Say we wish to broadcast (send) a couple of transactions via the Multicall contract.
/// let first_broadcast = contract.method::<_, H256>("setValue", "some value".to_owned())?;
/// let second_broadcast = contract.method::<_, H256>("setValue", "new value".to_owned())?;
/// multicall
///     .clear_calls()
///     .add_call(first_broadcast, false)
///     .add_call(second_broadcast, false);
///
/// // `await`ing the `send` method waits for the transaction to be broadcast, which also
/// // returns the transaction hash
/// let tx_receipt = multicall.send().await?.await.expect("tx dropped");
///
/// // you can also query ETH balances of multiple addresses
/// let address_1 = "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa".parse::<Address>()?;
/// let address_2 = "ffffffffffffffffffffffffffffffffffffffff".parse::<Address>()?;
///
/// multicall
///     .clear_calls()
///     .add_get_eth_balance(address_1, false)
///     .add_get_eth_balance(address_2, false);
/// let balances: (U256, U256) = multicall.call().await?;
///
/// # Ok(())
/// # }
/// ```
///
/// [`new`]: #method.new
/// [`new_with_chain`]: #method.new_with_chain
/// [`version`]: #method.version
/// [`block`]: #method.block
/// [`legacy`]: #method.legacy
/// [`add_call`]: #method.add_call
/// [`call`]: #method.call
/// [`send`]: #method.send
#[must_use = "Multicall does nothing unless you use `call` or `send`"]
pub struct Multicall<M> {
    /// The Multicall contract interface.
    pub contract: MulticallContract<M>,

    /// The version of which methods to use when making the contract call.
    pub version: MulticallVersion,

    /// Whether to use a legacy or a EIP-1559 transaction.
    pub legacy: bool,

    /// The `block` field of the Multicall aggregate call.
    pub block: Option<BlockNumber>,

    /// The internal call vector.
    calls: Vec<Call>,
}

// Manually implement Clone and Debug to avoid trait bounds.
impl<M> Clone for Multicall<M> {
    fn clone(&self) -> Self {
        Self {
            contract: self.contract.clone(),
            version: self.version,
            legacy: self.legacy,
            block: self.block,
            calls: self.calls.clone(),
        }
    }
}

impl<M> fmt::Debug for Multicall<M> {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        f.debug_struct("Multicall")
            .field("address", &self.contract.address())
            .field("version", &self.version)
            .field("legacy", &self.legacy)
            .field("block", &self.block)
            .field("calls", &self.calls)
            .finish()
    }
}

impl<M: Middleware> Multicall<M> {
    /// Creates a new Multicall instance from the provided client. If provided with an `address`,
    /// it instantiates the Multicall contract with that address, otherwise it defaults to
    /// [`constants::MULTICALL_ADDRESS`].
    ///
    /// # Errors
    ///
    /// Returns a [`error::MulticallError`] if the provider returns an error while getting
    /// `network_version`.
    ///
    /// # Panics
    ///
    /// If a `None` address is provided and the client's network is
    /// [not supported](constants::MULTICALL_SUPPORTED_CHAIN_IDS).
    pub async fn new(client: impl Into<Arc<M>>, address: Option<Address>) -> Result<Self, M> {
        let client = client.into();

        // Fetch chain id and the corresponding address of Multicall contract
        // preference is given to Multicall contract's address if provided
        // otherwise check the supported chain IDs for the client's chain ID
        let address: Address = match address {
            Some(addr) => addr,
            None => {
                let chain_id = client
                    .get_chainid()
                    .await
                    .map_err(ContractError::from_middleware_error)?
                    .as_u64();
                if !constants::MULTICALL_SUPPORTED_CHAIN_IDS.contains(&chain_id) {
                    return Err(error::MulticallError::InvalidChainId(chain_id))
                }
                constants::MULTICALL_ADDRESS
            }
        };

        // Instantiate the multicall contract
        let contract = MulticallContract::new(address, client);

        Ok(Self {
            version: MulticallVersion::Multicall3,
            legacy: false,
            block: None,
            calls: vec![],
            contract,
        })
    }

    /// Creates a new Multicall instance synchronously from the provided client and address or chain
    /// ID. Uses the [default multicall address](constants::MULTICALL_ADDRESS) if no address is
    /// provided.
    ///
    /// # Errors
    ///
    /// Returns a [`error::MulticallError`] if the provided chain_id is not in the
    /// [supported networks](constants::MULTICALL_SUPPORTED_CHAIN_IDS).
    ///
    /// # Panics
    ///
    /// If neither an address or chain_id are provided. Since this is not an async function, it will
    /// not be able to query `net_version` to check if it is supported by the default multicall
    /// address. Use new(client, None).await instead.
    pub fn new_with_chain_id(
        client: impl Into<Arc<M>>,
        address: Option<Address>,
        chain_id: Option<impl Into<u64>>,
    ) -> Result<Self, M> {
        // If no address is provided, check if chain_id is supported and use the default multicall
        // address.
        let address: Address = match (address, chain_id) {
            (Some(addr), _) => addr,
            (_, Some(chain_id)) => {
                let chain_id = chain_id.into();
                if !constants::MULTICALL_SUPPORTED_CHAIN_IDS.contains(&chain_id) {
                    return Err(error::MulticallError::InvalidChainId(chain_id))
                }
                constants::MULTICALL_ADDRESS
            }
            _ => {
                // Can't fetch chain_id from provider since we're not in an async function so we
                // panic instead.
                panic!("Must provide at least one of: address or chain ID.")
            }
        };

        // Instantiate the multicall contract
        let contract = MulticallContract::new(address, client.into());

        Ok(Self {
            version: MulticallVersion::Multicall3,
            legacy: false,
            block: None,
            calls: vec![],
            contract,
        })
    }

    /// Changes which functions to use when making the contract call. The default is 3. Version
    /// differences (adapted from [here](https://github.com/mds1/multicall#multicall---)):
    ///
    /// - Multicall (v1): This is the recommended version for simple calls. The original contract
    /// containing an aggregate method to batch calls. Each call returns only the return data and
    /// none are allowed to fail.
    ///
    /// - Multicall2 (v2): The same as Multicall, but provides additional methods that allow either
    /// all or no calls within the batch to fail. Included for backward compatibility. Use v3 to
    /// allow failure on a per-call basis.
    ///
    /// - Multicall3 (v3): This is the recommended version for allowing failing calls. It's cheaper
    /// to use (so you can fit more calls into a single request), and it adds an aggregate3 method
    /// so you can specify whether calls are allowed to fail on a per-call basis.
    ///
    /// Note: all these versions are available in the same contract address
    /// ([`constants::MULTICALL_ADDRESS`]) so changing version just changes the methods used,
    /// not the contract address.
    pub fn version(mut self, version: MulticallVersion) -> Self {
        self.version = version;
        self
    }

    /// Makes a legacy transaction instead of an EIP-1559 one.
    pub fn legacy(mut self) -> Self {
        self.legacy = true;
        self
    }

    /// Sets the `block` field of the Multicall aggregate call.
    pub fn block(mut self, block: impl Into<BlockNumber>) -> Self {
        self.block = Some(block.into());
        self
    }

    /// Appends a `call` to the list of calls of the Multicall instance.
    ///
    /// Version specific details:
    /// - `1`: `allow_failure` is ignored.
    /// - `>=2`: `allow_failure` specifies whether or not this call is allowed to revert in the
    ///   multicall.
    /// - `3`: Transaction values are used when broadcasting transactions with [`send`], otherwise
    ///   they are always ignored.
    ///
    /// [`send`]: #method.send
    pub fn add_call<D: Detokenize>(
        &mut self,
        call: ContractCall<M, D>,
        allow_failure: bool,
    ) -> &mut Self {
        let (to, data, value) = match call.tx {
            TypedTransaction::Legacy(tx) => (tx.to, tx.data, tx.value),
            TypedTransaction::Eip2930(tx) => (tx.tx.to, tx.tx.data, tx.tx.value),
            TypedTransaction::Eip1559(tx) => (tx.to, tx.data, tx.value),
        };
        if data.is_none() && !call.function.outputs.is_empty() {
            return self
        }
        if let Some(NameOrAddress::Address(target)) = to {
            let call = Call {
                target,
                data: data.unwrap_or_default(),
                value: value.unwrap_or_default(),
                allow_failure,
                function: call.function,
            };
            self.calls.push(call);
        }
        self
    }

    /// Appends multiple `call`s to the list of calls of the Multicall instance.
    ///
    /// See [`add_call`] for more details.
    ///
    /// [`add_call`]: #method.add_call
    pub fn add_calls<D: Detokenize>(
        &mut self,
        allow_failure: bool,
        calls: impl IntoIterator<Item = ContractCall<M, D>>,
    ) -> &mut Self {
        for call in calls {
            self.add_call(call, allow_failure);
        }
        self
    }

    /// Appends a `call` to the list of calls of the Multicall instance for querying the block hash
    /// of a given block number.
    ///
    /// Note: this call will return 0 if `block_number` is not one of the most recent 256 blocks.
    /// ([Reference](https://docs.soliditylang.org/en/latest/units-and-global-variables.html?highlight=blockhash#block-and-transaction-properties))
    pub fn add_get_block_hash(&mut self, block_number: impl Into<U256>) -> &mut Self {
        let call = self.contract.get_block_hash(block_number.into());
        self.add_call(call, false)
    }

    /// Appends a `call` to the list of calls of the Multicall instance for querying the current
    /// block number.
    pub fn add_get_block_number(&mut self) -> &mut Self {
        let call = self.contract.get_block_number();
        self.add_call(call, false)
    }

    /// Appends a `call` to the list of calls of the Multicall instance for querying the current
    /// block coinbase address.
    pub fn add_get_current_block_coinbase(&mut self) -> &mut Self {
        let call = self.contract.get_current_block_coinbase();
        self.add_call(call, false)
    }

    /// Appends a `call` to the list of calls of the Multicall instance for querying the current
    /// block difficulty.
    ///
    /// Note: in a post-merge environment, the return value of this call will be the output of the
    /// randomness beacon provided by the beacon chain.
    /// ([Reference](https://eips.ethereum.org/EIPS/eip-4399#abstract))
    pub fn add_get_current_block_difficulty(&mut self) -> &mut Self {
        let call = self.contract.get_current_block_difficulty();
        self.add_call(call, false)
    }

    /// Appends a `call` to the list of calls of the Multicall instance for querying the current
    /// block gas limit.
    pub fn add_get_current_block_gas_limit(&mut self) -> &mut Self {
        let call = self.contract.get_current_block_gas_limit();
        self.add_call(call, false)
    }

    /// Appends a `call` to the list of calls of the Multicall instance for querying the current
    /// block timestamp.
    pub fn add_get_current_block_timestamp(&mut self) -> &mut Self {
        let call = self.contract.get_current_block_timestamp();
        self.add_call(call, false)
    }

    /// Appends a `call` to the list of calls of the Multicall instance for querying the ETH
    /// balance of an address.
    pub fn add_get_eth_balance(
        &mut self,
        address: impl Into<Address>,
        allow_failure: bool,
    ) -> &mut Self {
        let call = self.contract.get_eth_balance(address.into());
        self.add_call(call, allow_failure)
    }

    /// Appends a `call` to the list of calls of the Multicall instance for querying the last
    /// block hash.
    pub fn add_get_last_block_hash(&mut self) -> &mut Self {
        let call = self.contract.get_last_block_hash();
        self.add_call(call, false)
    }

    /// Appends a `call` to the list of calls of the Multicall instance for querying the current
    /// block base fee.
    ///
    /// Note: this call will fail if the chain that it is called on does not implement the
    /// [BASEFEE opcode](https://eips.ethereum.org/EIPS/eip-3198).
    pub fn add_get_basefee(&mut self, allow_failure: bool) -> &mut Self {
        let call = self.contract.get_basefee();
        self.add_call(call, allow_failure)
    }

    /// Appends a `call` to the list of calls of the Multicall instance for querying the chain id.
    pub fn add_get_chain_id(&mut self) -> &mut Self {
        let call = self.contract.get_chain_id();
        self.add_call(call, false)
    }

    /// Clears the batch of calls from the Multicall instance.
    /// Re-use the already instantiated Multicall to send a different batch of transactions or do
    /// another aggregate query.
    ///
    /// # Examples
    ///
    /// ```no_run
    /// # async fn foo() -> Result<(), Box<dyn std::error::Error>> {
    /// # use ethers_core::{abi::Abi, types::{Address, H256}};
    /// # use ethers_providers::{Provider, Http};
    /// # use ethers_contract::{Multicall, Contract};
    /// # use std::{sync::Arc, convert::TryFrom};
    /// #
    /// # let client = Provider::<Http>::try_from("http://localhost:8545")?;
    /// # let client = Arc::new(client);
    /// #
    /// # let abi: Abi = serde_json::from_str("")?;
    /// # let address = "eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee".parse::<Address>()?;
    /// # let contract = Contract::<Provider<Http>>::new(address, abi, client.clone());
    /// #
    /// # let broadcast_1 = contract.method::<_, H256>("setValue", "some value".to_owned())?;
    /// # let broadcast_2 = contract.method::<_, H256>("setValue", "new value".to_owned())?;
    /// #
    /// let mut multicall = Multicall::new(client, None).await?;
    /// multicall
    ///     .add_call(broadcast_1, false)
    ///     .add_call(broadcast_2, false);
    ///
    /// let _tx_receipt = multicall.send().await?.await.expect("tx dropped");
    ///
    /// # let call_1 = contract.method::<_, String>("getValue", ())?;
    /// # let call_2 = contract.method::<_, Address>("lastSender", ())?;
    /// multicall
    ///     .clear_calls()
    ///     .add_call(call_1, false)
    ///     .add_call(call_2, false);
    /// // Version 1:
    /// let return_data: (String, Address) = multicall.call().await?;
    /// // Version 2 and above (each call returns also the success status as the first element):
    /// let return_data: ((bool, String), (bool, Address)) = multicall.call().await?;
    /// # Ok(())
    /// # }
    /// ```
    pub fn clear_calls(&mut self) -> &mut Self {
        self.calls.clear();
        self
    }

    /// Queries the Ethereum blockchain using `eth_call`, but via the Multicall contract.
    ///
    /// For handling calls that have the same result type, see [`call_array`].
    ///
    /// For handling each call's result individually, see [`call_raw`].
    ///
    /// [`call_raw`]: #method.call_raw
    /// [`call_array`]: #method.call_array
    ///
    /// # Errors
    ///
    /// Returns a [`error::MulticallError`] if there are any errors in the RPC call or while
    /// detokenizing the tokens back to the expected return type.
    ///
    /// Returns an error if any call failed, even if `allow_failure` was set, or if the return data
    /// was empty.
    ///
    /// # Examples
    ///
    /// The return type must be annotated as a tuple when calling this method:
    ///
    /// ```no_run
    /// # async fn foo() -> Result<(), Box<dyn std::error::Error>> {
    /// # use ethers_core::types::{U256, Address};
    /// # use ethers_providers::{Provider, Http};
    /// # use ethers_contract::Multicall;
    /// # use std::convert::TryFrom;
    /// #
    /// # let client = Provider::<Http>::try_from("http://localhost:8545")?;
    /// #
    /// # let multicall = Multicall::new(client, None).await?;
    /// // If the Solidity function calls has the following return types:
    /// // 1. `returns (uint256)`
    /// // 2. `returns (string, address)`
    /// // 3. `returns (bool)`
    /// let result: (U256, (String, Address), bool) = multicall.call().await?;
    /// // or using the turbofish syntax:
    /// let result = multicall.call::<(U256, (String, Address), bool)>().await?;
    /// # Ok(())
    /// # }
    /// ```
    pub async fn call<T: Tokenizable>(&self) -> Result<T, M> {
        let results = self.call_raw().await?;
        let tokens = results
            .into_iter()
            .map(|res| {
                res.map_err(|data| {
                    error::MulticallError::ContractError(ContractError::Revert(data))
                })
            })
            .collect::<Result<_, _>>()?;
        T::from_token(Token::Tuple(tokens)).map_err(Into::into)
    }

    /// Queries the Ethereum blockchain using `eth_call`, but via the Multicall contract, assuming
    /// that every call returns same type.
    ///
    /// # Errors
    ///
    /// Returns a [`error::MulticallError`] if there are any errors in the RPC call or while
    /// detokenizing the tokens back to the expected return type.
    ///
    /// Returns an error if any call failed, even if `allow_failure` was set, or if the return data
    /// was empty.
    ///
    /// # Examples
    ///
    /// The return type must be annotated while calling this method:
    ///
    /// ```no_run
    /// # async fn foo() -> Result<(), Box<dyn std::error::Error>> {
    /// # use ethers_core::types::{U256, Address};
    /// # use ethers_providers::{Provider, Http};
    /// # use ethers_contract::Multicall;
    /// # use std::convert::TryFrom;
    /// #
    /// # let client = Provider::<Http>::try_from("http://localhost:8545")?;
    /// #
    /// # let multicall = Multicall::new(client, None).await?;
    /// // If the all Solidity function calls `returns (uint256)`:
    /// let result: Vec<U256> = multicall.call_array().await?;
    /// # Ok(())
    /// # }
    /// ```
    pub async fn call_array<T: Tokenizable>(&self) -> Result<Vec<T>, M> {
        self.call_raw()
            .await?
            .into_iter()
            .map(|res| {
                res.map_err(|data| {
                    error::MulticallError::ContractError(ContractError::Revert(data))
                })
                .and_then(|token| T::from_token(token).map_err(Into::into))
            })
            .collect()
    }

    /// Queries the Ethereum blockchain using `eth_call`, but via the Multicall contract.
    ///
    /// Returns a vector of `Result<Token, Bytes>` for each call added to the Multicall:
    /// `Err(Bytes)` if the individual call failed while allowed or the return data was empty,
    /// `Ok(Token)` otherwise.
    ///
    /// If the Multicall version is 1, this will always be a vector of `Ok`.
    ///
    /// # Errors
    ///
    /// Returns a [`error::MulticallError`] if there are any errors in the RPC call.
    ///
    /// # Examples
    ///
    /// ```no_run
    /// # async fn foo() -> Result<(), Box<dyn std::error::Error>> {
    /// # use ethers_core::types::{U256, Address};
    /// # use ethers_providers::{Provider, Http};
    /// # use ethers_contract::Multicall;
    /// # use std::convert::TryFrom;
    /// #
    /// # let client = Provider::<Http>::try_from("http://localhost:8545")?;
    /// #
    /// # let multicall = Multicall::new(client, None).await?;
    /// // The consumer of the API is responsible for detokenizing the results
    /// let tokens = multicall.call_raw().await?;
    /// # Ok(())
    /// # }
    /// ```
    pub async fn call_raw(&self) -> Result<Vec<StdResult<Token, Bytes>>, M> {
        // Different call result types based on version
        match self.version {
            // Wrap the return data with `success: true` since version 1 reverts if any call failed
            MulticallVersion::Multicall => {
                let call = self.as_aggregate();
                let (_, bytes) = ContractCall::call(&call).await?;
                self.parse_call_result(
                    bytes
                        .into_iter()
                        .map(|return_data| MulticallResult { success: true, return_data }),
                )
            }
            // Same result type (`MulticallResult`)
            MulticallVersion::Multicall2 | MulticallVersion::Multicall3 => {
                let call = if self.version.is_v2() {
                    self.as_try_aggregate()
                } else {
                    self.as_aggregate_3()
                };
                let results = ContractCall::call(&call).await?;
                self.parse_call_result(results.into_iter())
            }
        }
    }

    /// For each call and its `return_data`: if `success` is true, parses `return_data` with the
    /// call's function outputs, otherwise returns the bytes in `Err`.
    fn parse_call_result(
        &self,
        return_data: impl Iterator<Item = MulticallResult>,
    ) -> Result<Vec<StdResult<Token, Bytes>>, M> {
        let mut results = Vec::with_capacity(self.calls.len());
        for (call, MulticallResult { success, return_data }) in self.calls.iter().zip(return_data) {
            let result = if !success || return_data.is_empty() {
                // v2: In the function call to `tryAggregate`, the `allow_failure` check
                // is done on a per-transaction basis, and we set this transaction-wide
                // check to true when *any* call is allowed to fail. If this is true
                // then a call that is not allowed to revert (`call.allow_failure`) may
                // still do so because of other calls that are in the same multicall
                // aggregate.
                if !success && !call.allow_failure {
                    return Err(error::MulticallError::IllegalRevert)
                }

                Err(return_data)
            } else {
                let mut res_tokens = call.function.decode_output(return_data.as_ref())?;
                Ok(if res_tokens.len() == 1 {
                    res_tokens.pop().unwrap()
                } else {
                    Token::Tuple(res_tokens)
                })
            };
            results.push(result);
        }
        Ok(results)
    }

    /// Signs and broadcasts a batch of transactions by using the Multicall contract as proxy,
    /// returning the pending transaction.
    ///
    /// Note: this method will broadcast a transaction from an account, meaning it must have
    /// sufficient funds for gas and transaction value.
    ///
    /// # Errors
    ///
    /// Returns a [`error::MulticallError`] if there are any errors in the RPC call.
    ///
    /// # Examples
    ///
    /// ```no_run
    /// # async fn foo() -> Result<(), Box<dyn std::error::Error>> {
    /// # use ethers_providers::{Provider, Http};
    /// # use ethers_contract::Multicall;
    /// # use std::convert::TryFrom;
    /// # let client = Provider::<Http>::try_from("http://localhost:8545")?;
    /// # let multicall = Multicall::new(client, None).await?;
    /// let tx_hash = multicall.send().await?;
    /// # Ok(())
    /// # }
    /// ```
    pub async fn send(&self) -> Result<PendingTransaction<'_, M::Provider>, M> {
        let tx = match self.version {
            MulticallVersion::Multicall => self.as_aggregate().tx,
            MulticallVersion::Multicall2 => self.as_try_aggregate().tx,
            MulticallVersion::Multicall3 => self.as_aggregate_3_value().tx,
        };
        let client: &M = self.contract.client_ref();
        client.send_transaction(tx, self.block.map(Into::into)).await.map_err(|e| {
            error::MulticallError::ContractError(ContractError::from_middleware_error(e))
        })
    }

    /// v1
    #[inline]
    fn as_aggregate(&self) -> ContractCall<M, (U256, Vec<Bytes>)> {
        // Map the calls vector into appropriate types for `aggregate` function
        let calls: Vec<Multicall1Call> = self
            .calls
            .clone()
            .into_iter()
            .map(|call| Multicall1Call { target: call.target, call_data: call.data })
            .collect();

        // Construct the ContractCall for `aggregate` function to broadcast the transaction
        let contract_call = self.contract.aggregate(calls);

        self.set_call_flags(contract_call)
    }

    /// v2
    #[inline]
    fn as_try_aggregate(&self) -> ContractCall<M, Vec<MulticallResult>> {
        let mut allow_failure = false;
        // Map the calls vector into appropriate types for `try_aggregate` function
        let calls: Vec<Multicall1Call> = self
            .calls
            .clone()
            .into_iter()
            .map(|call| {
                // Allow entire call failure if at least one call is allowed to fail.
                // To avoid iterating multiple times, equivalent of:
                // self.calls.iter().any(|call| call.allow_failure)
                allow_failure |= call.allow_failure;
                Multicall1Call { target: call.target, call_data: call.data }
            })
            .collect();

        // Construct the ContractCall for `try_aggregate` function to broadcast the transaction
        let contract_call = self.contract.try_aggregate(!allow_failure, calls);

        self.set_call_flags(contract_call)
    }

    /// v3
    #[inline]
    fn as_aggregate_3(&self) -> ContractCall<M, Vec<MulticallResult>> {
        // Map the calls vector into appropriate types for `aggregate_3` function
        let calls: Vec<Multicall3Call> = self
            .calls
            .clone()
            .into_iter()
            .map(|call| Multicall3Call {
                target: call.target,
                call_data: call.data,
                allow_failure: call.allow_failure,
            })
            .collect();

        // Construct the ContractCall for `aggregate_3` function to broadcast the transaction
        let contract_call = self.contract.aggregate_3(calls);

        self.set_call_flags(contract_call)
    }

    /// v3 + values (only .send())
    #[inline]
    fn as_aggregate_3_value(&self) -> ContractCall<M, Vec<MulticallResult>> {
        // Map the calls vector into appropriate types for `aggregate_3_value` function
        let mut total_value = U256::zero();
        let calls: Vec<Multicall3CallValue> = self
            .calls
            .clone()
            .into_iter()
            .map(|call| {
                total_value += call.value;
                Multicall3CallValue {
                    target: call.target,
                    call_data: call.data,
                    allow_failure: call.allow_failure,
                    value: call.value,
                }
            })
            .collect();

        if total_value.is_zero() {
            // No value is being sent
            self.as_aggregate_3()
        } else {
            // Construct the ContractCall for `aggregate_3_value` function to broadcast the
            // transaction
            let contract_call = self.contract.aggregate_3_value(calls);

            self.set_call_flags(contract_call).value(total_value)
        }
    }

    /// Sets the block and legacy flags on a [ContractCall] if they were set on Multicall.
    fn set_call_flags<D: Detokenize>(&self, mut call: ContractCall<M, D>) -> ContractCall<M, D> {
        if let Some(block) = self.block {
            call.block = Some(block.into());
        }

        if self.legacy {
            call.legacy()
        } else {
            call
        }
    }
}
