use alloy::{
    primitives::{bytes, Address, Bytes, Keccak256, U256},
    uint,
};
use trevm::revm::{
    state::{Account, AccountInfo, Bytecode, EvmState, EvmStorageSlot},
    Database, DatabaseCommit,
};

alloy::sol! {
    /// ERC20 token interface for testing.
    #[sol(rpc)]
    #[derive(Debug)]
    interface ERC20 {
        function name() external view returns (string memory);
        function symbol() external view returns (string memory);
        function decimals() external view returns (uint8);
        function totalSupply() external view returns (uint256);
        function balanceOf(address account) external view returns (uint256);
        function transfer(address to, uint256 amount) external returns (bool);
        function allowance(address owner, address spender) external view returns (uint256);
        function approve(address spender, uint256 amount) external returns (bool);
        function transferFrom(address from, address to, uint256 amount) external returns (bool);

        event Transfer(address indexed from, address indexed to, uint256 value);
        event Approval(address indexed owner, address indexed spender, uint256 value);
    }
}

/// Slot at which the balances mapping is stored.
pub const BALANCES_SLOT: U256 = uint!(0_U256);
/// Slot at which the allowances mapping is stored.
pub const ALLOWANCES_SLOT: U256 = uint!(1_U256);
/// Slot at which the total supply is stored.
pub const TOTAL_SUPPLY_SLOT: U256 = uint!(2_U256);
/// Slot at which the token name is stored.
pub const NAME_SLOT: U256 = uint!(3_U256);
/// Slot at which the token symbol is stored.
pub const SYMBOL_SLOT: U256 = uint!(4_U256);
/// Slot at which the token minter is stored.
pub const MINTER_SLOT: U256 = uint!(5_U256);

/// Address of the token minter.
pub const MINTER: U256 =
    uint!(0x00000000000000000000000000000000000000000000746f6b656e61646d696e_U256);
/// WBTC Contract name.
pub const WBTC_NAME: U256 =
    uint!(0x5772617070656420425443000000000000000000000000000000000000000016_U256);
/// WBTC Contract symbol.
pub const WBTC_SYMBOL: U256 =
    uint!(0x5742544300000000000000000000000000000000000000000000000000000008_U256);
/// WETH Contract name.
pub const WETH_NAME: U256 =
    uint!(0x577261707065642045746865720000000000000000000000000000000000001a_U256);
/// WETH Contract symbol.
pub const WETH_SYMBOL: U256 =
    uint!(0x5745544800000000000000000000000000000000000000000000000000000008_U256);

/// Token Contract bytecode, from pecorino genesis.
pub const TOKEN_BYTECODE: Bytes = bytes!("[encoded data, 4912 characters]");

/// Deploys a token contract at the specified address in the given database.
pub fn deploy_token_at<Db: Database + DatabaseCommit>(
    db: &mut Db,
    addr: Address,
    name: U256,
    symbol: U256,
) -> Result<(), Db::Error> {
    let info: AccountInfo =
        db.basic(addr)?.unwrap_or_default().with_code(Bytecode::new_legacy(TOKEN_BYTECODE));

    let acct = Account { info, transaction_id: 0, ..Default::default() }
        .with_storage(
            [
                (NAME_SLOT, EvmStorageSlot::new(name, 0)),
                (SYMBOL_SLOT, EvmStorageSlot::new(symbol, 0)),
                (MINTER_SLOT, EvmStorageSlot::new(MINTER, 0)),
            ]
            .into_iter(),
        )
        .with_touched_mark();

    let changes: EvmState = [(addr, acct)].into_iter().collect();
    db.commit(changes);

    Ok(())
}

/// Deploys a WETH token contract at the specified address in the given database.
pub fn deploy_weth_at<Db: Database + DatabaseCommit>(
    db: &mut Db,
    addr: Address,
) -> Result<(), Db::Error> {
    deploy_token_at(db, addr, WETH_NAME, WETH_SYMBOL)
}

/// Deploys a WBTC token contract at the specified address in the given database.
pub fn deploy_wbtc_at<Db: Database + DatabaseCommit>(
    db: &mut Db,
    addr: Address,
) -> Result<(), Db::Error> {
    deploy_token_at(db, addr, WBTC_NAME, WBTC_SYMBOL)
}

fn mapping_slot(base: U256, key: Address) -> U256 {
    let mut hasher = Keccak256::new();
    hasher.update([0u8; 12]);
    hasher.update(key);
    hasher.update(base.to_be_bytes::<32>());
    U256::from_be_bytes::<32>(hasher.finalize().into())
}

/// Computes the storage slot for the balance of the given owner.
pub fn balance_slot_for(owner: Address) -> U256 {
    mapping_slot(BALANCES_SLOT, owner)
}

/// Computes the storage slot for the allowance mapping for the given owner and spender.
pub fn allowances_slot_for(owner: Address, spender: Address) -> U256 {
    let inner_map_loc = mapping_slot(ALLOWANCES_SLOT, owner);
    mapping_slot(inner_map_loc, spender)
}
