defmodule EVM.Interface.Mock.MockAccountInterface do
  @moduledoc """
  Simple implementation of an AccountInterface.
  """

  defstruct account_map: %{},
            contract_result: %{
              gas: nil,
              sub_state: nil,
              output: nil
            }

  def new(account_map \\ %{}, contract_result \\ %{}) do
    %__MODULE__{
      account_map: account_map,
      contract_result: contract_result
    }
  end
end

defimpl EVM.Interface.AccountInterface, for: EVM.Interface.Mock.MockAccountInterface do
  alias Block.Header

  @spec account_exists?(EVM.Interface.AccountInterface.t(), EVM.address()) :: boolean()
  def account_exists?(mock_account_interface, address) do
    !!get_account(mock_account_interface, :binary.decode_unsigned(address))
  end

  @spec get_account_balance(EVM.Interface.AccountInterface.t(), EVM.address()) ::
          nil | EVM.Wei.t()
  def get_account_balance(mock_account_interface, address) do
    account = get_account(mock_account_interface, address)

    if account do
      account.balance
    else
      nil
    end
  end

  @spec add_wei(EVM.Interface.AccountInterface.t(), EVM.address(), integer()) ::
          EVM.Interface.AccountInterface.t()
  def add_wei(mock_account_interface, address, value) do
    account = get_account(mock_account_interface, address) || new_account()

    put_account(mock_account_interface, address, %{account | balance: account.balance + value})
  end

  @spec transfer(EVM.Interface.AccountInterface.t(), EVM.address(), EVM.address(), integer()) ::
          EVM.Interface.AccountInterface.t()
  def transfer(mock_account_interface, from, to, value) do
    mock_account_interface
    |> add_wei(from, -value)
    |> add_wei(to, value)
  end

  @spec get_account_code(EVM.Interface.AccountInterface.t(), EVM.address()) :: nil | binary()
  def get_account_code(mock_account_interface, address) do
    account = get_account(mock_account_interface, address)

    if account do
      account.code
    else
      <<>>
    end
  end

  defp get_account(mock_account_interface, address) do
    Map.get(mock_account_interface.account_map, address)
  end

  @spec increment_account_nonce(EVM.Interface.AccountInterface.t(), EVM.address()) ::
          {EVM.Interface.AccountInterface.t(), integer()}
  def increment_account_nonce(mock_account_interface, address) do
    {
      mock_account_interface,
      Map.get(mock_account_interface.account_map, address).nonce + 1
    }
  end

  # TODO: Integrate new interface
  @spec get_storage(EVM.Interface.AccountInterface.t(), EVM.address(), integer()) ::
          {:ok, integer()} | :account_not_found | :key_not_found
  def get_storage(mock_account_interface, address, key) do
    case mock_account_interface.account_map[address] do
      nil ->
        :account_not_found

      account ->
        case account[:storage][key] do
          nil -> :key_not_found
          value -> {:ok, value}
        end
    end
  end

  @spec put_storage(EVM.Interface.AccountInterface.t(), EVM.address(), integer(), integer()) ::
          EVM.Interface.AccountInterface.t()
  def put_storage(mock_account_interface, address, key, value) do
    account = get_account(mock_account_interface, address)

    account =
      if account do
        update_storage(account, key, value)
      else
        new_account(%{
          storage: %{key => value}
        })
      end

    put_account(mock_account_interface, address, account)
  end

  defp update_storage(account, key, value) do
    put_in(account, [:storage, key], value)
  end

  defp put_account(mock_account_interface, address, account) do
    %{
      mock_account_interface
      | account_map: Map.put(mock_account_interface.account_map, address, account)
    }
  end

  defp new_account(opts \\ %{}) do
    Map.merge(
      %{
        storage: %{},
        nonce: 0,
        balance: 0
      },
      opts
    )
  end

  @spec suicide_account(EVM.Interface.AccountInterface.t(), EVM.address()) ::
          EVM.Interface.AccountInterface.t()
  def suicide_account(mock_account_interface, address) do
    account_map =
      mock_account_interface.account_map
      |> Map.delete(address)

    %{mock_account_interface | account_map: account_map}
  end

  @spec get_account_nonce(EVM.Interface.AccountInterface.t(), EVM.address()) :: integer()
  def get_account_nonce(mock_account_interface, address) do
    get_in(mock_account_interface.account_map, [address, :nonce])
  end

  @spec dump_storage(EVM.Interface.AccountInterface.t()) :: %{EVM.address() => EVM.val()}
  def dump_storage(mock_account_interface) do
    for {address, account} <- mock_account_interface.account_map, into: %{} do
      storage =
        account[:storage]
        |> Enum.reject(fn {_key, value} -> value == 0 end)
        |> Enum.into(%{})

      {address, storage}
    end
  end

  @spec message_call(
          EVM.Interface.AccountInterface.t(),
          EVM.address(),
          EVM.address(),
          EVM.address(),
          EVM.address(),
          EVM.Gas.t(),
          EVM.Gas.gas_price(),
          EVM.Wei.t(),
          EVM.Wei.t(),
          binary(),
          integer(),
          Header.t()
        ) :: {EVM.Interface.AccountInterface.t(), EVM.Gas.t(), EVM.SubState.t(), EVM.VM.output()}
  def message_call(
        mock_account_interface,
        _sender,
        _originator,
        _recipient,
        _contract,
        _available_gas,
        _gas_price,
        _value,
        _apparent_value,
        _data,
        _stack_depth,
        _block_header
      ) do
    {
      mock_account_interface,
      mock_account_interface.contract_result[:gas],
      mock_account_interface.contract_result[:sub_state],
      mock_account_interface.contract_result[:output]
    }
  end

  @spec create_contract(
          EVM.Interface.AccountInterface.t(),
          EVM.address(),
          EVM.address(),
          EVM.Gas.t(),
          EVM.Gas.gas_price(),
          EVM.Wei.t(),
          EVM.MachineCode.t(),
          integer(),
          Header.t()
        ) :: {EVM.Gas.t(), EVM.Interface.AccountInterface.t(), EVM.SubState.t()}
  def create_contract(
        mock_account_interface,
        _sender,
        _originator,
        _available_gas,
        _gas_price,
        _endowment,
        _init_code,
        _stack_depth,
        _block_header
      ) do
    {
      mock_account_interface,
      mock_account_interface.contract_result[:gas],
      mock_account_interface.contract_result[:sub_state]
    }
  end

  @spec new_contract_address(EVM.Interface.AccountInterface.t(), EVM.address(), integer()) ::
          EVM.address()
  def new_contract_address(_mock_account_interface, address, _nonce) do
    address
  end
end
