defmodule ExW3.Rpc do
  import ExW3.Client

  @type invalid_hex_string_error :: ExW3.Utils.invalid_hex_string_error()
  @type request_error :: Ethereumex.Client.Behaviour.error()
  @type opts :: {:url, String.t()}
  @type hex_block_number :: String.t()
  @type latest :: String.t()
  @type earliest :: String.t()
  @type pending :: String.t()

  @doc "returns all available accounts"
  @spec accounts() :: list()
  @spec accounts([opts]) :: list()
  def accounts(opts \\ []) do
    case call_client(:eth_accounts, [opts]) do
      {:ok, accounts} -> accounts
      err -> err
    end
  end

  @doc "Returns the current block number"
  @spec block_number() :: {:ok, non_neg_integer} | {:error, ExW3.Utils.invalid_hex_string()}
  @spec block_number([opts]) :: {:ok, non_neg_integer} | {:error, ExW3.Utils.invalid_hex_string()}
  def block_number(opts \\ []) do
    case call_client(:eth_block_number, [opts]) do
      {:ok, hex_block_number} -> ExW3.Utils.hex_to_integer(hex_block_number)
      err -> err
    end
  end

  @doc "Returns current balance of account"
  @spec balance(binary()) :: integer() | {:error, any()}
  @spec balance(binary(), [opts]) :: integer() | {:error, any()}
  def balance(account, opts \\ []) do
    case call_client(:eth_get_balance, [account, "latest", opts]) do
      {:ok, hex_balance} ->
        {:ok, balance} = ExW3.Utils.hex_to_integer(hex_balance)
        balance

      err ->
        err
    end
  end

  @doc "Returns transaction receipt for specified transaction hash(id)"
  @spec tx_receipt(binary()) :: {:ok, map()} | {:error, any()}
  def tx_receipt(tx_hash) do
    case call_client(:eth_get_transaction_receipt, [tx_hash]) do
      {:ok, nil} ->
        {:error, :not_mined}

      {:ok, receipt} ->
        normalized_receipt =
          ExW3.Normalize.transform_to_integer(receipt, ~w(blockNumber cumulativeGasUsed gasUsed))

        {:ok, Map.merge(receipt, normalized_receipt)}

      err ->
        {:error, err}
    end
  end

  @doc "Returns block data for specified block number"
  @spec block(integer()) :: any() | {:error, any()}
  def block(block_number) do
    case call_client(:eth_get_block_by_number, [block_number, true]) do
      {:ok, block} -> block
      err -> err
    end
  end

  @doc "Creates a new filter, returns filter id. For more sophisticated use, prefer ExW3.Contract.filter."
  @spec new_filter(map()) :: binary() | {:error, any()}
  def new_filter(map) do
    case call_client(:eth_new_filter, [map]) do
      {:ok, filter_id} -> filter_id
      err -> err
    end
  end

  @doc "Gets event changes (logs) by filter. Unlike ExW3.Contract.get_filter_changes it does not return the data in a formatted way"
  @spec get_filter_changes(binary()) :: any()
  def get_filter_changes(filter_id) do
    case call_client(:eth_get_filter_changes, [filter_id]) do
      {:ok, changes} -> changes
      err -> err
    end
  end

  @type log_filter :: %{
          optional(:address) => String.t(),
          optional(:fromBlock) => hex_block_number | latest | earliest | pending,
          optional(:toBlock) => hex_block_number | latest | earliest | pending,
          optional(:topics) => [String.t()],
          optional(:blockhash) => String.t()
        }

  @spec get_logs(log_filter, [opts]) :: {:ok, list} | {:error, term} | request_error
  def get_logs(filter, opts \\ []) do
    with {:ok, _} = result <- call_client(:eth_get_logs, [filter, opts]) do
      result
    else
      err -> err
    end
  end

  @doc "Uninstalls filter from the ethereum node"
  @spec uninstall_filter(binary()) :: boolean() | {:error, any()}
  def uninstall_filter(filter_id) do
    case call_client(:eth_uninstall_filter, [filter_id]) do
      {:ok, result} -> result
      err -> err
    end
  end

  @doc "Mines number of blocks specified. Default is 1"
  @spec mine(integer()) :: any() | {:error, any()}
  def mine(num_blocks \\ 1) do
    for _ <- 0..(num_blocks - 1) do
      call_client(:request, ["evm_mine", [], []])
    end
  end

  @doc "Using the personal api, returns list of accounts."
  @spec personal_list_accounts(list()) :: {:ok, list()} | {:error, any()}
  def personal_list_accounts(opts \\ []) do
    call_client(:request, ["personal_listAccounts", [], opts])
  end

  @doc "Using the personal api, this method creates a new account with the passphrase, and returns new account address."
  @spec personal_new_account(binary(), list()) :: {:ok, binary()} | {:error, any()}
  def personal_new_account(password, opts \\ []) do
    call_client(:request, ["personal_newAccount", [password], opts])
  end

  @doc "Using the personal api, this method unlocks account using the passphrase provided, and returns a boolean."
  @spec personal_unlock_account(binary(), list()) :: {:ok, boolean()} | {:error, any()}
  ### E.g. ExW3.personal_unlock_account(["0x1234","Password",30], [])
  def personal_unlock_account(params, opts \\ []) do
    call_client(:request, ["personal_unlockAccount", params, opts])
  end

  @doc "Using the personal api, this method sends a transaction and signs it in one call, and returns a transaction id hash."
  @spec personal_send_transaction(map(), binary(), list()) :: {:ok, binary()} | {:error, any()}
  def personal_send_transaction(param_map, passphrase, opts \\ []) do
    call_client(:request, ["personal_sendTransaction", [param_map, passphrase], opts])
  end

  @doc "Using the personal api, this method signs a transaction, and returns the signed transaction."
  @spec personal_sign_transaction(map(), binary(), list()) :: {:ok, map()} | {:error, any()}
  def personal_sign_transaction(param_map, passphrase, opts \\ []) do
    call_client(:request, ["personal_signTransaction", [param_map, passphrase], opts])
  end

  @doc "Using the personal api, this method calculates an Ethereum specific signature, and returns that signature."
  @spec personal_sign(binary(), binary(), binary(), list()) :: {:ok, binary()} | {:error, any()}
  def personal_sign(data, address, passphrase, opts \\ []) do
    call_client(:request, ["personal_sign", [data, address, passphrase], opts])
  end

  @doc "Using the personal api, this method returns the address associated with the private key that was used to calculate the signature with personal_sign."
  @spec personal_ec_recover(binary(), binary(), []) :: {:ok, binary()} | {:error, any()}
  def personal_ec_recover(data0, data1, opts \\ []) do
    call_client(:request, ["personal_ecRecover", [data0, data1], opts])
  end

  @doc "Calculates an Ethereum specific signature and signs the data provided, using the accounts private key"
  @spec eth_sign(binary(), binary(), list()) :: {:ok, binary()} | {:error, any()}
  def eth_sign(data0, data1, opts \\ []) do
    call_client(:request, ["eth_sign", [data0, data1], opts])
  end

  @doc "Simple eth_call to client. Recommended to use ExW3.Contract.call instead."
  @spec eth_call(list()) :: any()
  def eth_call(arguments) do
    call_client(:eth_call, arguments)
  end

  @doc "Simple eth_send_transaction. Recommended to use ExW3.Contract.send instead."
  @spec eth_send(list()) :: any()
  def eth_send(arguments) do
    call_client(:eth_send_transaction, arguments)
  end
end
