defmodule Ethers.EventFilter do
  @moduledoc """
  Event Filter struct and helper functions to work with the event filters
  """

  @typedoc """
  Holds event filter topics, the event selector and the default address.

  Can be passed in to `Ethers.get_logs/2` filter and fetch the logs.
  """
  @type t :: %__MODULE__{
          topics: [binary()],
          selector: ABI.FunctionSelector.t(),
          default_address: nil | Ethers.Types.t_address()
        }

  @enforce_keys [:topics, :selector]
  defstruct [:topics, :selector, :default_address]

  @doc false
  @spec new([binary()], ABI.FunctionSelector.t(), Ethers.Types.t_address() | nil) :: t()
  def new(topics, selector, default_address) do
    %__MODULE__{
      topics: topics,
      selector: selector,
      default_address: default_address
    }
  end

  @doc false
  @spec to_map(t() | map(), Keyword.t()) :: map()
  def to_map(%__MODULE__{} = tx_data, overrides) do
    tx_data
    |> event_filter_map()
    |> to_map(overrides)
  end

  def to_map(event_filter, overrides) do
    Enum.into(overrides, event_filter)
  end

  defp event_filter_map(%{selector: %{type: :event}} = event_filter) do
    %{topics: event_filter.topics}
    |> maybe_add_address(event_filter.default_address)
  end

  defp maybe_add_address(tx_map, nil), do: tx_map
  defp maybe_add_address(tx_map, address), do: Map.put(tx_map, :address, address)

  defimpl Inspect do
    import Inspect.Algebra

    alias Ethers.Utils

    def inspect(
          %{selector: selector, topics: [_t0 | topics], default_address: default_address},
          opts
        ) do
      default_address =
        case default_address do
          nil ->
            []

          _ ->
            [
              line(),
              color("default_address: ", :default, opts),
              color(inspect(default_address), :string, opts)
            ]
        end

      argument_doc =
        case argument_doc(selector, topics, opts) do
          [] ->
            [
              color("(", :operator, opts),
              color(")", :operator, opts)
            ]

          argument_doc ->
            [
              color("(", :operator, opts),
              nest(concat([break("") | argument_doc]), 2),
              break(""),
              color(")", :operator, opts)
            ]
        end

      inner =
        concat(
          [
            break(""),
            color("event", :atom, opts),
            " ",
            color(selector.function, :call, opts)
          ] ++ argument_doc ++ default_address
        )

      concat([
        color("#Ethers.EventFilter<", :map, opts),
        nest(inner, 2),
        break(""),
        color(">", :map, opts)
      ])
    end

    defp input_names(selector) do
      if Enum.count(selector.types) == Enum.count(selector.input_names) do
        selector.input_names
      else
        1..Enum.count(selector.types)
        |> Enum.map(fn _ -> nil end)
      end
    end

    defp argument_doc(selector, topics, opts),
      do:
        argument_doc(
          selector.types,
          input_names(selector),
          selector.inputs_indexed,
          topics,
          [],
          opts
        )

    defp argument_doc(types, input_names, inputs_indexed, topics, acc, opts)

    defp argument_doc([], _, _, _, acc, opts) do
      Enum.reverse(acc)
      |> Enum.intersperse(concat(color(",", :operator, opts), break(" ")))
    end

    defp argument_doc(
           [type | types],
           [name | input_names],
           [true | inputs_indexed],
           [topic | topics],
           acc,
           opts
         ) do
      doc =
        [
          color(ABI.FunctionSelector.encode_type(type), :atom, opts),
          " ",
          color("indexed", nil, opts),
          if(name, do: " "),
          if(name, do: color(name, :variable, opts)),
          " ",
          if(is_nil(topic), do: color("any", :string, opts), else: human_topic(type, topic))
        ]
        |> Enum.reject(&is_nil/1)
        |> concat()

      argument_doc(types, input_names, inputs_indexed, topics, [doc | acc], opts)
    end

    defp argument_doc(
           [type | types],
           [name | input_names],
           [false | inputs_indexed],
           topics,
           acc,
           opts
         ) do
      doc =
        [
          color(ABI.FunctionSelector.encode_type(type), :atom, opts),
          if(name, do: " "),
          if(name, do: color(name, :variable, opts))
        ]
        |> Enum.reject(&is_nil/1)
        |> concat()

      argument_doc(types, input_names, inputs_indexed, topics, [doc | acc], opts)
    end

    defp human_topic(type, topic) do
      hashed? =
        case type do
          type when type in [:string, :bytes] -> true
          {:array, _} -> true
          {:array, _, _} -> true
          {:tuple, _} -> true
          _ -> false
        end

      if hashed? do
        "(hashed) #{inspect(topic)}"
      else
        [value] =
          Utils.hex_decode!(topic)
          |> ABI.TypeDecoder.decode([type])

        inspect(Utils.human_arg(value, type))
      end
    end
  end
end
