// Copyright 2015-2020 Parity Technologies (UK) Ltd.
// This file is part of OpenEthereum.

// OpenEthereum is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.

// OpenEthereum is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
// GNU General Public License for more details.

// You should have received a copy of the GNU General Public License
// along with OpenEthereum.  If not, see <http://www.gnu.org/licenses/>.

//! Utility functions.
//!
//! Contains small functions used by the AuRa engine that are not strictly limited to that scope.

use std::fmt;

use client::{traits::EngineClient, BlockChainClient};
use ethabi::{self, FunctionOutputDecoder};
use ethabi_contract::use_contract;
use ethereum_types::{Address, U256};
use log::{debug, error};
use types::{header::Header, ids::BlockId};

/// A contract bound to a client and block number.
///
/// A bound contract is a combination of a `Client` reference, a `BlockId` and a contract `Address`.
/// These three parts are enough to call a contract's function; return values are automatically
/// decoded.
pub struct BoundContract<'a> {
    client: &'a dyn EngineClient,
    block_id: BlockId,
    contract_addr: Address,
}

/// Contract call failed error.
#[derive(Debug)]
pub enum CallError {
    /// The call itself failed.
    CallFailed(String),
    /// Decoding the return value failed or the decoded value was a failure.
    DecodeFailed(ethabi::Error),
    /// The passed in client reference could not be upgraded to a `BlockchainClient`.
    NotFullClient,
}

impl<'a> fmt::Debug for BoundContract<'a> {
    fn fmt(&self, fmt: &mut fmt::Formatter) -> fmt::Result {
        fmt.debug_struct("BoundContract")
            .field("client", &(self.client as *const dyn EngineClient))
            .field("block_id", &self.block_id)
            .field("contract_addr", &self.contract_addr)
            .finish()
    }
}

impl<'a> BoundContract<'a> {
    /// Create a new `BoundContract`.
    pub fn new(
        client: &dyn EngineClient,
        block_id: BlockId,
        contract_addr: Address,
    ) -> BoundContract {
        BoundContract {
            client,
            block_id,
            contract_addr,
        }
    }

    /// Perform a function call to an Ethereum machine that doesn't create a transaction or change the state.
    ///
    /// Runs a constant function call on `client`. The `call` value can be serialized by calling any
    /// api function generated by the `use_contract!` macro. This does not create any transactions, it only produces a
    /// result based on the state at the current block: It is constant in the sense that it does not alter the EVM
    /// state.
    pub fn call_const<D>(&self, call: (ethabi::Bytes, D)) -> Result<D::Output, CallError>
    where
        D: ethabi::FunctionOutputDecoder,
    {
        let (data, output_decoder) = call;

        let call_return = self
            .client
            .as_full_client()
            .ok_or(CallError::NotFullClient)?
            .call_contract(self.block_id, self.contract_addr, data)
            .map_err(CallError::CallFailed)?;

        // Decode the result and return it.
        output_decoder
            .decode(call_return.as_slice())
            .map_err(CallError::DecodeFailed)
    }
}

use_contract!(contract, "res/contracts/block_gas_limit.json");

pub fn block_gas_limit(
    full_client: &dyn BlockChainClient,
    header: &Header,
    address: Address,
) -> Option<U256> {
    let (data, decoder) = contract::functions::block_gas_limit::call();
    let value = full_client.call_contract(BlockId::Hash(*header.parent_hash()), address, data).map_err(|err| {
		error!(target: "block_gas_limit", "Contract call failed. Not changing the block gas limit. {:?}", err);
	}).ok()?;
    if value.is_empty() {
        debug!(target: "block_gas_limit", "Contract call returned nothing. Not changing the block gas limit.");
        None
    } else {
        decoder.decode(&value).ok()
    }
}
