defmodule ETH.Query do
  import ETH.Utils
  alias Ethereumex.HttpClient

  def block_number do
    case HttpClient.eth_block_number() do
      {:ok, hex_block_number} -> {:ok, convert_to_number(hex_block_number)}
      error -> error
    end
  end

  def block_number! do
    {:ok, hex_block_number} = HttpClient.eth_block_number()

    hex_block_number |> convert_to_number
  end

  def syncing, do: HttpClient.eth_syncing()

  def syncing! do
    {:ok, result} = HttpClient.eth_syncing()

    result
  end

  def get_accounts, do: HttpClient.eth_accounts()

  def get_accounts! do
    {:ok, accounts} = HttpClient.eth_accounts()

    accounts
  end

  def gas_price do
    case HttpClient.eth_gas_price() do
      {:ok, hex_gas_price} -> {:ok, convert_to_number(hex_gas_price)}
      error -> error
    end
  end

  def gas_price! do
    {:ok, hex_gas_price} = HttpClient.eth_gas_price()

    convert_to_number(hex_gas_price)
  end

  # TODO: test this one
  def call(call_params, state \\ "latest"), do: HttpClient.eth_call(call_params, state)

  def call!(call_params, state \\ "latest") do
    {:ok, result} = HttpClient.eth_call(call_params, state)

    result
  end

  def get_block do
    case HttpClient.eth_get_block_by_number(
           "0x" <> Hexate.encode(block_number!()),
           true
         ) do
      {:ok, raw_block_details} -> {:ok, convert_block_details(raw_block_details)}
      error -> error
    end
  end

  def get_block(block_number) when is_number(block_number) do
    case HttpClient.eth_get_block_by_number("0x" <> Hexate.encode(block_number), true) do
      {:ok, raw_block_details} -> {:ok, convert_block_details(raw_block_details)}
      error -> error
    end
  end

  def get_block(block_hash) do
    case HttpClient.eth_get_block_by_hash(block_hash, true) do
      {:ok, raw_block_details} -> {:ok, convert_block_details(raw_block_details)}
      error -> error
    end
  end

  def get_block! do
    block_hash = "0x" <> Hexate.encode(block_number!())
    {:ok, raw_block_details} = HttpClient.eth_get_block_by_number(block_hash, true)

    convert_block_details(raw_block_details)
  end

  def get_block!(block_number) when is_number(block_number) do
    block_hash = "0x" <> Hexate.encode(block_number)

    {:ok, raw_block_details} = HttpClient.eth_get_block_by_number(block_hash, true)

    convert_block_details(raw_block_details)
  end

  def get_block!(block_hash) do
    {:ok, raw_block_details} = HttpClient.eth_get_block_by_hash(block_hash, true)

    convert_block_details(raw_block_details)
  end

  def get_balance(param, denomination \\ :ether, state \\ "latest")

  def get_balance(wallet, denomination, state) when is_map(wallet) do
    case HttpClient.eth_get_balance(wallet.eth_address, state) do
      {:ok, hex_balance} ->
        balance =
          hex_balance
          |> convert_to_number
          |> convert(denomination)

        {:ok, balance}

      error ->
        error
    end
  end

  def get_balance(eth_address, denomination, state) do
    case HttpClient.eth_get_balance(eth_address, state) do
      {:ok, hex_balance} ->
        balance =
          hex_balance
          |> convert_to_number
          |> convert(denomination)

        {:ok, balance}

      error ->
        error
    end
  end

  def get_balance!(param, denomination \\ :ether, state \\ "latest")

  def get_balance!(wallet, denomination, state) when is_map(wallet) do
    {:ok, hex_balance} = HttpClient.eth_get_balance(wallet.eth_address, state)

    hex_balance
    |> convert_to_number
    |> convert(denomination)
  end

  def get_balance!(eth_address, denomination, state) do
    {:ok, hex_balance} = HttpClient.eth_get_balance(eth_address, state)

    hex_balance
    |> convert_to_number
    |> convert(denomination)
  end

  def estimate_gas(transaction \\ %{data: ""}, denomination \\ :wei)

  def estimate_gas(transaction = %{to: _to, data: _data}, denomination) do
    case HttpClient.eth_estimate_gas(transaction) do
      {:ok, hex_gas_estimate} ->
        {:ok, hex_gas_estimate |> convert_to_number |> convert(denomination) |> round}

      error ->
        error
    end
  end

  def estimate_gas!(transaction \\ %{data: ""}, denomaination \\ :wei)

  def estimate_gas!(transaction = %{to: _to, data: _data}, denomination) do
    {:ok, hex_gas_estimate} = HttpClient.eth_estimate_gas(transaction)

    hex_gas_estimate |> convert_to_number |> convert(denomination) |> round
  end

  def convert_transaction_log(log) do
    Enum.reduce(log, %{}, fn tuple, acc ->
      {key, value} = tuple

      case key do
        "blockNumber" -> Map.put(acc, :block_number, convert_to_number(value))
        "logIndex" -> Map.put(acc, :log_index, convert_to_number(value))
        "transactionIndex" -> Map.put(acc, :transaction_index, convert_to_number(value))
        "transactionLogIndex" -> Map.put(acc, :transaction_log_index, convert_to_number(value))
        _ -> Map.put(acc, key |> Macro.underscore() |> String.to_atom(), value)
      end
    end)
  end

  def convert_transaction_details(transaction) do
    Enum.reduce(transaction, %{}, fn tuple, acc ->
      {key, value} = tuple

      case key do
        "nonce" -> Map.put(acc, :nonce, convert_to_number(value))
        "blockNumber" -> Map.put(acc, :block_number, convert_to_number(value))
        "transactionIndex" -> Map.put(acc, :transaction_index, convert_to_number(value))
        "value" -> Map.put(acc, :value, convert_to_number(value))
        "gasPrice" -> Map.put(acc, :gas_price, convert_to_number(value))
        "gas" -> Map.put(acc, :gas, convert_to_number(value))
        _ -> Map.put(acc, key |> Macro.underscore() |> String.to_atom(), value)
      end
    end)
  end

  def convert_block_details(result) do
    result
    |> Enum.reduce(%{}, fn tuple, acc ->
      {key, value} = tuple

      case key do
        "number" ->
          Map.put(acc, :number, convert_to_number(value))

        "size" ->
          Map.put(acc, :size, convert_to_number(value))

        "gasLimit" ->
          Map.put(acc, :gas_limit, convert_to_number(value))

        "gasUsed" ->
          Map.put(acc, :gas_used, convert_to_number(value))

        "timestamp" ->
          Map.put(acc, :timestamp, convert_to_number(value))

        "difficulty" ->
          Map.put(acc, :difficulty, convert_to_number(value))

        "totalDifficulty" ->
          Map.put(acc, :total_difficulty, convert_to_number(value))

        "transactions" ->
          Map.put(
            acc,
            String.to_atom(key),
            Enum.map(value, fn transaction ->
              convert_transaction_details(transaction)
            end)
          )

        _ ->
          Map.put(acc, key |> Macro.underscore() |> String.to_atom(), value)
      end
    end)
  end

  defp convert_to_number(result) do
    result
    |> String.slice(2..-1)
    |> Hexate.to_integer()
  end
end
