use crate::{
    call::{ContractCall, ContractError},
    Lazy,
};
use ethers_core::{
    abi::{AbiDecode, Detokenize, Function, Token},
    types::{Address, BlockNumber, Bytes, Chain, NameOrAddress, TxHash, H160, U256},
};
use ethers_providers::Middleware;
use std::{convert::TryFrom, sync::Arc};

pub mod multicall_contract;
use multicall_contract::multicall_3::{
    Call as Multicall1Call, Call3 as Multicall3Call, Call3Value as Multicall3CallValue,
    Result as MulticallResult,
};

// Export the contract interface
pub use multicall_contract::multicall_3::Multicall3 as MulticallContract;

/// The Multicall3 contract address that is deployed in [`MULTICALL_SUPPORTED_CHAIN_IDS`]:
/// [`0xcA11bde05977b3631167028862bE2a173976CA11`](https://etherscan.io/address/0xcA11bde05977b3631167028862bE2a173976CA11)
pub const MULTICALL_ADDRESS: Address = H160([
    0xca, 0x11, 0xbd, 0xe0, 0x59, 0x77, 0xb3, 0x63, 0x11, 0x67, 0x02, 0x88, 0x62, 0xbe, 0x2a, 0x17,
    0x39, 0x76, 0xca, 0x11,
]);

/// The chain IDs that [`MULTICALL_ADDRESS`] has been deployed to.
/// Taken from: <https://github.com/mds1/multicall#multicall3-contract-addresses>
pub static MULTICALL_SUPPORTED_CHAIN_IDS: Lazy<[U256; 48]> = Lazy::new(|| {
    use Chain::*;
    [
        U256::from(Mainnet),                  // Mainnet
        U256::from(Kovan),                    // Kovan
        U256::from(Rinkeby),                  // Rinkeby
        U256::from(Goerli),                   // Goerli
        U256::from(Ropsten),                  // Ropsten
        U256::from(Sepolia),                  // Sepolia
        U256::from(Optimism),                 // Optimism
        U256::from(OptimismGoerli),           // OptimismGoerli
        U256::from(OptimismKovan),            // OptimismKovan
        U256::from(Arbitrum),                 // Arbitrum
        U256::from(ArbitrumGoerli),           // ArbitrumGoerli,
        U256::from(ArbitrumTestnet),          // Arbitrum Rinkeby
        U256::from(Polygon),                  // Polygon
        U256::from(PolygonMumbai),            // PolygonMumbai
        U256::from(XDai),                     // XDai
        U256::from(Chiado),                   // ChiadoTestnet
        U256::from(Avalanche),                // Avalanche
        U256::from(AvalancheFuji),            // AvalancheFuji
        U256::from(FantomTestnet),            // FantomTestnet
        U256::from(Fantom),                   // Fantom
        U256::from(BinanceSmartChain),        // BinanceSmartChain
        U256::from(BinanceSmartChainTestnet), // BinanceSmartChainTestnet
        U256::from(Moonbeam),                 // Moonbeam
        U256::from(Moonriver),                // Moonriver
        U256::from(Moonbase),                 // Moonbase
        U256::from(1666600000),               // Harmony0
        U256::from(1666600001),               // Harmony1
        U256::from(1666600002),               // Harmony2
        U256::from(1666600003),               // Harmony3
        U256::from(Cronos),                   // Cronos
        U256::from(122),                      // Fuse
        U256::from(19),                       // Songbird
        U256::from(16),                       // CostonTestnet
        U256::from(288),                      // Boba
        U256::from(Aurora),                   // Aurora
        U256::from(592),                      // Astar
        U256::from(66),                       // OKC
        U256::from(128),                      // Heco
        U256::from(1088),                     // Metis
        U256::from(Rsk),                      // Rsk
        U256::from(31),                       // RskTestnet
        U256::from(Evmos),                    // Evmos
        U256::from(EvmosTestnet),             // EvmosTestnet
        U256::from(71402),                    // Godwoken
        U256::from(71401),                    // GodwokenTestnet
        U256::from(8217),                     // Klaytn
        U256::from(2001),                     // Milkomeda
        U256::from(321),                      // KCC
    ]
});

#[derive(Debug, thiserror::Error)]
pub enum MulticallError<M: Middleware> {
    #[error(transparent)]
    ContractError(#[from] ContractError<M>),

    #[error("Chain ID {0} is currently not supported by Multicall. Provide an address instead.")]
    InvalidChainId(U256),

    #[error("Illegal revert: Multicall2 call reverted when it wasn't allowed to.")]
    IllegalRevert,
}

pub type Result<T, M> = std::result::Result<T, MulticallError<M>>;

/// 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 {
    Multicall = 1,
    Multicall2 = 2,
    #[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) -> std::result::Result<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.")),
        }
    }
}

/// 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
/// [`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("https://kovan.infura.io/v3/c60b0bb42f8a4c6481ecd229eddaca27")?;
///
/// // 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 the Kovan testnet, 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?.version(MulticallVersion::Multicall);
/// 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?;
///
/// // using Multicall2 (version 2) or Multicall3 (version 3) differs when parsing `.call()` results
/// multicall = multicall.version(MulticallVersion::Multicall3);
///
/// // each call returns the results in a tuple, with the success status as the first element
/// let _return_data: ((bool, String), (bool, 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_hash = multicall.send().await?;
/// let _tx_receipt = PendingTransaction::new(tx_hash, &client).await?;
///
/// // you can also query ETH balances of multiple addresses
/// let address_1 = "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa".parse::<Address>()?;
/// let address_2 = "ffffffffffffffffffffffffffffffffffffffff".parse::<Address>()?;
///
/// // using version 1
/// multicall = multicall.version(MulticallVersion::Multicall);
/// multicall
///     .clear_calls()
///     .add_get_eth_balance(address_1, false)
///     .add_get_eth_balance(address_2, false);
/// let _balances: (U256, U256) = multicall.call().await?;
///
/// // or with version 2 and above
/// multicall = multicall.version(MulticallVersion::Multicall3);
/// multicall
///     .clear_calls()
///     .add_get_eth_balance(address_1, false)
///     .add_get_eth_balance(address_2, false);
/// let _balances: ((bool, U256), (bool, 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
#[derive(Clone)]
#[must_use = "Multicall does nothing unless you use `call` or `send`"]
pub struct Multicall<M> {
    /// The Multicall contract interface.
    pub contract: MulticallContract<M>,
    version: MulticallVersion,
    legacy: bool,
    block: Option<BlockNumber>,
    calls: Vec<Call>,
}

// Manually implement Debug due to Middleware trait bounds.
impl<M: Middleware> std::fmt::Debug for Multicall<M> {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        f.debug_struct("Multicall")
            .field("version", &self.version)
            .field("legacy", &self.legacy)
            .field("block", &self.block)
            .field("calls", &self.calls)
            .field("contract", &self.contract)
            .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
    /// [`MULTICALL_ADDRESS`].
    ///
    /// # Errors
    ///
    /// Returns a [`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](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::MiddlewareError)?;
                if !MULTICALL_SUPPORTED_CHAIN_IDS.contains(&chain_id) {
                    return Err(MulticallError::InvalidChainId(chain_id))
                }
                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](MULTICALL_ADDRESS) if no address is provided.
    ///
    /// # Errors
    ///
    /// Returns a [`MulticallError`] if the provided chain_id is not in the
    /// [supported networks](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<U256>>,
    ) -> 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 {
            Some(addr) => addr,
            None => {
                // Can't fetch chain_id from provider since we're not in an async function so we
                // panic instead.
                let chain_id =
                    chain_id.expect("Must provide at least one of: address or chain ID.").into();
                if !MULTICALL_SUPPORTED_CHAIN_IDS.contains(&chain_id) {
                    return Err(MulticallError::InvalidChainId(chain_id))
                }
                MULTICALL_ADDRESS
            }
        };

        // 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 ([`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 {
        match (call.tx.to(), call.tx.data()) {
            (Some(NameOrAddress::Address(target)), Some(data)) => {
                let call = Call {
                    target: *target,
                    data: data.clone(),
                    value: call.tx.value().cloned().unwrap_or_default(),
                    allow_failure,
                    function: call.function,
                };
                self.calls.push(call);
                self
            }
            _ => self,
        }
    }

    /// Appends multiple `call`s 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_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_hash = multicall.send().await?;
    ///
    /// # 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.
    ///
    /// Note: this method _does not_ send a transaction from your account.
    ///
    /// # Errors
    ///
    /// Returns a [`MulticallError`] if there are any errors in the RPC call or while detokenizing
    /// the tokens back to the expected return type.
    ///
    /// # Panics
    ///
    /// If more than the maximum number of supported calls are added (16). The maximum limit is
    /// constrained due to tokenization/detokenization support for tuples.
    ///
    /// # 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 Solidity function calls has the following return types:
    /// // 1. `returns (uint256)`
    /// // 2. `returns (string, address)`
    /// // 3. `returns (bool)`
    /// // Version 1:
    /// let result: (U256, (String, Address), bool) = multicall.call().await?;
    /// // Version 2 and above (each call returns also the success status as the first element):
    /// let result: ((bool, U256), (bool, (String, Address)), (bool, bool)) = multicall.call().await?;
    /// # Ok(())
    /// # }
    /// ```
    pub async fn call<D: Detokenize>(&self) -> Result<D, M> {
        assert!(self.calls.len() <= 16, "Cannot decode more than 16 calls");
        let tokens = self.call_raw().await?;
        let tokens = vec![Token::Tuple(tokens)];
        let data = D::from_tokens(tokens).map_err(ContractError::DetokenizationError)?;
        Ok(data)
    }

    /// Queries the Ethereum blockchain using `eth_call`, but via the Multicall contract, assuming
    /// that every call returns same data type.
    ///
    /// Note: this method _does not_ send a transaction from your account.
    ///
    /// # Errors
    ///
    /// Returns a [`MulticallError`] if there are any errors in the RPC call or while detokenizing
    /// the tokens back to the expected return type.
    ///
    /// # 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().await?;
    /// # Ok(())
    /// # }
    /// ```
    pub async fn call_array<D: Detokenize>(&self) -> Result<Vec<D>, M> {
        let tokens = self.call_raw().await?;
        let res: std::result::Result<Vec<D>, ContractError<M>> = tokens
            .into_iter()
            .map(|token| D::from_tokens(vec![token]).map_err(ContractError::DetokenizationError))
            .collect();

        Ok(res?)
    }

    /// Queries the Ethereum blockchain using `eth_call`, but via the Multicall contract and
    /// without detokenization.
    ///
    /// # Errors
    ///
    /// Returns a [`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
    /// // as the results will be a Vec<Token>
    /// let tokens = multicall.call_raw().await?;
    /// # Ok(())
    /// # }
    /// ```
    ///
    /// Note: this method _does not_ send a transaction from your account
    ///
    /// [`ContractError<M>`]: crate::ContractError<M>
    pub async fn call_raw(&self) -> Result<Vec<Token>, M> {
        // Different call result types based on version
        let tokens: Vec<Token> = match self.version {
            MulticallVersion::Multicall => {
                let call = self.as_aggregate();
                let (_, return_data) = call.call().await?;
                self.calls
                    .iter()
                    .zip(&return_data)
                    .map(|(call, bytes)| {
                        // Always return an empty Bytes token for calls that return no data
                        if bytes.is_empty() {
                            Ok(Token::Bytes(Default::default()))
                        } else {
                            let mut tokens = call
                                .function
                                .decode_output(bytes)
                                .map_err(ContractError::DecodingError)?;
                            Ok(match tokens.len() {
                                0 => Token::Tuple(vec![]),
                                1 => tokens.remove(0),
                                _ => Token::Tuple(tokens),
                            })
                        }
                    })
                    .collect::<Result<Vec<Token>, M>>()?
            }
            // Same result type (`MulticallResult`)
            v @ (MulticallVersion::Multicall2 | MulticallVersion::Multicall3) => {
                let is_v2 = v == MulticallVersion::Multicall2;
                let call = if is_v2 { self.as_try_aggregate() } else { self.as_aggregate_3() };
                let return_data = call.call().await?;
                self.calls
                    .iter()
                    .zip(return_data.into_iter())
                    .map(|(call, res)| {
                        let bytes = &res.return_data;
                        // Always return an empty Bytes token for calls that return no data
                        let res_token: Token = if bytes.is_empty() {
                            Token::Bytes(Default::default())
                        } else if res.success {
                            // Decode using call.function
                            let mut res_tokens = call
                                .function
                                .decode_output(bytes)
                                .map_err(ContractError::DecodingError)?;
                            match res_tokens.len() {
                                0 => Token::Tuple(vec![]),
                                1 => res_tokens.remove(0),
                                _ => Token::Tuple(res_tokens),
                            }
                        } else {
                            // Call reverted

                            // 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 !call.allow_failure {
                                return Err(MulticallError::IllegalRevert)
                            }

                            // Decode with "Error(string)" (0x08c379a0)
                            if bytes.len() >= 4 && bytes[..4] == [0x08, 0xc3, 0x79, 0xa0] {
                                Token::String(
                                    String::decode(&bytes[4..]).map_err(ContractError::AbiError)?,
                                )
                            } else {
                                Token::Bytes(bytes.to_vec())
                            }
                        };

                        // (bool, (...))
                        Ok(Token::Tuple(vec![Token::Bool(res.success), res_token]))
                    })
                    .collect::<Result<Vec<Token>, M>>()?
            }
        };

        Ok(tokens)
    }

    /// Signs and broadcasts a batch of transactions by using the Multicall contract as proxy,
    /// returning the transaction hash once the transaction confirms.
    ///
    /// Note: this method will broadcast a transaction from an account, meaning it must have
    /// sufficient funds for gas and transaction value.
    ///
    /// # Errors
    ///
    /// Returns a [`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<TxHash, M> {
        // Broadcast transaction and return the transaction hash
        // TODO: Can we make this return a PendingTransaction directly instead?
        // Seems hard due to `returns a value referencing data owned by the current function`

        // running clippy --fix on this throws E0597
        #[allow(clippy::let_and_return)]
        let tx_hash = match self.version {
            MulticallVersion::Multicall => {
                let call = self.as_aggregate();
                let hash = *call.send().await?;
                hash
            }
            MulticallVersion::Multicall2 => {
                let call = self.as_try_aggregate();
                let hash = *call.send().await?;
                hash
            }
            MulticallVersion::Multicall3 => {
                let call = self.as_aggregate_3_value();
                let hash = *call.send().await?;
                hash
            }
        };

        Ok(tx_hash)
    }

    /// v1
    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
    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
    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())
    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 = call.block(block);
        }

        if self.legacy {
            call = call.legacy();
        }

        call
    }
}
