defmodule Hologram.Compiler.Encoder do
  @moduledoc false

  if Application.compile_env(:hologram, :debug_encoder) do
    use Interceptor.Annotated,
      config: %{
        {Hologram.Compiler.Encoder, :encode_ir, 2} => [
          on_success: {Hologram.Compiler.Encoder, :debug, 3},
          on_error: {Hologram.Compiler.Encoder, :debug, 3}
        ]
      }
  end

  alias Hologram.Commons.IntegerUtils
  alias Hologram.Commons.StringUtils
  alias Hologram.Compiler.Context
  alias Hologram.Compiler.IR
  alias Hologram.Reflection

  @doc """
  Encodes Elixir or Erlang alias as JavaScript class name.

  ## Examples

      iex> encode_as_class_name(Aaa.Bbb.Ccc)
      "Elixir_Aaa_Bbb_Ccc"

      iex> encode_as_class_name(:erlang)
      "Erlang"
      
      iex> encode_as_class_name(:aaa_bbb)
      "Erlang_Aaa_Bbb"      
  """
  @spec encode_as_class_name(module | atom) :: String.t()
  def encode_as_class_name(alias_atom)

  def encode_as_class_name(:erlang), do: "Erlang"

  def encode_as_class_name(alias_atom) do
    module_segments =
      alias_atom
      |> to_string()
      |> String.split([".", "_"])

    class_segments =
      if hd(module_segments) == "Elixir" do
        module_segments
      else
        ["Erlang" | module_segments]
      end

    Enum.map_join(class_segments, "_", &:string.titlecase/1)
  end

  @doc """
  Encodes an Elixir function into a JavaScript statement.
  """
  @spec encode_elixir_function(
          String.t(),
          atom,
          non_neg_integer,
          :public | :private,
          list(IR.FunctionClause.t()),
          Context.t()
        ) :: String.t()
  def encode_elixir_function(module_name, function, arity, visibility, clauses, context) do
    clauses_js = encode_as_array(clauses, context)

    ~s/Interpreter.defineElixirFunction("#{module_name}", "#{function}", #{arity}, "#{visibility}", #{clauses_js});/
  end

  @doc """
  Extracts JavaScript source code for the given ported Erlang function and generates interpreter function definition JavaScript statetement.
  """
  @spec encode_erlang_function(module, atom, integer, String.t()) :: String.t()
  def encode_erlang_function(module, function, arity, erlang_source_dir) do
    file_path =
      if module == :erlang do
        "#{erlang_source_dir}/erlang.mjs"
      else
        "#{erlang_source_dir}/#{module}.mjs"
      end

    source_code =
      if File.exists?(file_path) do
        extract_erlang_function_source_code(file_path, function, arity)
      else
        nil
      end

    if source_code do
      normalized_source_code = StringUtils.normalize_newlines(source_code)

      ~s/Interpreter.defineErlangFunction("#{module}", "#{function}", #{arity}, #{normalized_source_code});/
    else
      ~s/Interpreter.defineNotImplementedErlangFunction("#{module}", "#{function}", #{arity});/
    end
  end

  @doc """
  Encodes Elixir IR to JavaScript source code.

  ## Examples

      iex> ir = %IR.ListType{
      ...>   data: [
      ...>     %IR.IntegerType{value: 1},
      ...>     %IR.AtomType{value: :abc}
      ...>   ]
      ...> }
      iex> encode_ir(ir, %Context{})
      "Type.list([Type.integer(1), Type.atom(\"abc\")])"
  """
  @intercept true
  @spec encode_ir(IR.t(), Context.t()) :: String.t()
  def encode_ir(ir, context)

  def encode_ir(%IR.AnonymousFunctionCall{function: function, args: args}, context) do
    function_js = encode_ir(function, context)
    args_js = encode_as_array(args, context)

    "Interpreter.callAnonymousFunction(#{function_js}, #{args_js})"
  end

  def encode_ir(
        %IR.AnonymousFunctionType{
          arity: arity,
          captured_function: nil,
          captured_module: nil,
          clauses: clauses
        },
        context
      ) do
    clauses_js = encode_as_array(clauses, context)
    "Type.anonymousFunction(#{arity}, #{clauses_js}, context)"
  end

  def encode_ir(
        %IR.AnonymousFunctionType{
          arity: arity,
          captured_function: captured_function,
          captured_module: captured_module,
          clauses: clauses
        },
        context
      ) do
    captured_function_js = encode_as_string(captured_function, true)

    captured_module_str =
      if Reflection.elixir_module?(captured_module) do
        Reflection.module_name(captured_module)
      else
        ":#{captured_module}"
      end

    captured_module_js = encode_as_string(captured_module_str, true)

    clauses_js = encode_as_array(clauses, context)

    "Type.functionCapture(#{captured_module_js}, #{captured_function_js}, #{arity}, #{clauses_js}, context)"
  end

  def encode_ir(%IR.AtomType{value: value}, _context) do
    encode_primitive_type(:atom, value, true)
  end

  # See: https://hexdocs.pm/elixir/Kernel.SpecialForms.html#%3C%3C%3E%3E/1
  def encode_ir(
        %IR.BitstringSegment{value: %IR.StringType{value: value}, modifiers: modifiers},
        context
      ) do
    value_str = encode_primitive_type(:string, value, true)
    encode_bitstring_segment(value_str, modifiers, context)
  end

  # See: https://hexdocs.pm/elixir/Kernel.SpecialForms.html#%3C%3C%3E%3E/1
  def encode_ir(
        %IR.BitstringSegment{value: value, modifiers: modifiers},
        context
      ) do
    value_str = encode_ir(value, context)
    encode_bitstring_segment(value_str, modifiers, context)
  end

  def encode_ir(%IR.BitstringType{segments: segments}, %{pattern?: true} = context) do
    segments
    |> encode_bitstring_segments(context)
    |> StringUtils.wrap("Type.bitstringPattern([", "])")
  end

  def encode_ir(%IR.BitstringType{segments: segments}, %{pattern?: false} = context) do
    segments
    |> encode_bitstring_segments(context)
    |> StringUtils.wrap("Type.bitstring([", "])")
  end

  def encode_ir(%IR.Block{} = block, context) do
    "(#{encode_closure(block, context)})(context)"
  end

  def encode_ir(%IR.Case{condition: condition, clauses: clauses}, context) do
    condition_js =
      case condition do
        %IR.Block{} = block -> encode_closure(block, context)
        expr -> encode_ir(expr, context)
      end

    clauses_js = encode_as_array(clauses, context)

    "Interpreter.case(#{condition_js}, #{clauses_js}, context)"
  end

  def encode_ir(%IR.Clause{} = clause, context) do
    match = encode_ir(clause.match, %{context | pattern?: true})
    guards = encode_as_array(clause.guards, context, &encode_closure/2)
    body = encode_closure(clause.body, context)

    "{match: #{match}, guards: #{guards}, body: #{body}}"
  end

  def encode_ir(%IR.Comprehension{} = comprehension, context) do
    generators = encode_as_array(comprehension.generators, context)
    filters = encode_as_array(comprehension.filters, context)
    collectable = encode_ir(comprehension.collectable, context)
    unique = comprehension.unique.value
    mapper = encode_closure(comprehension.mapper, context)

    "Interpreter.comprehension(#{generators}, #{filters}, #{collectable}, #{unique}, #{mapper}, context)"
  end

  def encode_ir(%IR.ComprehensionFilter{expression: expr}, context) do
    encode_closure(expr, context)
  end

  def encode_ir(%IR.Cond{clauses: clauses_ir}, context) do
    clauses_js = encode_as_array(clauses_ir, context)
    "Interpreter.cond(#{clauses_js}, context)"
  end

  def encode_ir(%IR.CondClause{condition: condition_ir, body: body_ir}, context) do
    condition_js = encode_closure(condition_ir, context)
    body_js = encode_closure(body_ir, context)

    "{condition: #{condition_js}, body: #{body_js}}"
  end

  def encode_ir(%IR.ConsOperator{head: head, tail: tail}, %{pattern?: true} = context) do
    "Type.consPattern(#{encode_ir(head, context)}, #{encode_ir(tail, context)})"
  end

  def encode_ir(%IR.ConsOperator{head: head, tail: tail}, %{pattern?: false} = context) do
    "Interpreter.consOperator(#{encode_ir(head, context)}, #{encode_ir(tail, context)})"
  end

  def encode_ir(%IR.DotOperator{left: left, right: right}, context) do
    left_js = encode_ir(left, context)
    right_js = encode_ir(right, context)

    "Interpreter.dotOperator(#{left_js}, #{right_js})"
  end

  def encode_ir(%IR.FloatType{value: value}, _context) do
    encode_primitive_type(:float, value, false)
  end

  def encode_ir(%IR.FunctionClause{} = clause, context) do
    params_array = encode_as_array(clause.params, %{context | pattern?: true})
    params_closure = "(context) => #{params_array}"

    guards = encode_as_array(clause.guards, context, &encode_closure/2)
    body = encode_closure(clause.body, context)

    "{params: #{params_closure}, guards: #{guards}, body: #{body}}"
  end

  def encode_ir(%IR.IntegerType{value: value}, _context) do
    encode_primitive_type(:integer, "#{value}n", false)
  end

  def encode_ir(%IR.ListType{data: data}, context) do
    data_str = encode_as_array(data, context)
    "Type.list(#{data_str})"
  end

  def encode_ir(
        %IR.LocalFunctionCall{function: function, args: args},
        %{module: module} = context
      ) do
    module_ir = %IR.AtomType{value: module}
    encode_named_function_call(module_ir, function, args, context)
  end

  def encode_ir(%IR.MapType{data: data}, context) do
    data
    |> Enum.sort()
    |> Enum.map_join(", ", fn {key, value} ->
      "[" <> encode_ir(key, context) <> ", " <> encode_ir(value, context) <> "]"
    end)
    |> StringUtils.wrap("Type.map([", "])")
  end

  def encode_ir(%IR.MatchOperator{left: left, right: right}, %{match_operator?: true} = context) do
    left = encode_ir(left, %{context | pattern?: true})
    right = encode_ir(right, context)

    "Interpreter.matchOperator(#{right}, #{left}, context)"
  end

  def encode_ir(%IR.MatchOperator{left: left, right: right}, context) do
    left = encode_ir(left, %{context | match_operator?: true, pattern?: true})
    right = encode_ir(right, %{context | match_operator?: true})

    "Interpreter.matchOperator(#{right}, #{left}, context)"
  end

  def encode_ir(%IR.MatchPlaceholder{}, _context) do
    "Type.matchPlaceholder()"
  end

  def encode_ir(%IR.ModuleAttributeOperator{name: name}, _context) do
    encode_var("@#{name}", nil)
  end

  def encode_ir(%IR.ModuleDefinition{module: module} = module_def, context) do
    module_name = Reflection.module_name(module.value)

    module_def
    |> IR.aggregate_module_funs()
    |> Enum.reduce([], fn {{function, arity}, {visibility, clauses}}, acc ->
      [encode_elixir_function(module_name, function, arity, visibility, clauses, context) | acc]
    end)
    |> Enum.reverse()
    |> Enum.join("\n\n")
  rescue
    error ->
      message =
        StringUtils.normalize_newlines("""
        can't encode #{Reflection.module_name(module.value)} module definition
        #{Exception.message(error)}\
        """)

      reraise RuntimeError, [message: message], __STACKTRACE__
  end

  # See info about the internal structure of PIDs: https://stackoverflow.com/a/262179/13040586
  def encode_ir(%IR.PIDType{value: value}, context) do
    segments =
      value
      |> :erlang.pid_to_list()
      |> List.delete_at(0)
      |> List.delete_at(-1)
      |> to_string()
      |> String.split(".")
      |> Enum.map(&IntegerUtils.parse!/1)

    encode_identifier(:pid, value, segments, context)
  end

  def encode_ir(%IR.PinOperator{variable: variable}, context) do
    encode_ir(variable, %{context | pattern?: false})
  end

  def encode_ir(%IR.PortType{value: value}, context) do
    segments =
      value
      |> :erlang.port_to_list()
      |> Enum.drop(6)
      |> List.delete_at(-1)
      |> to_string()
      |> String.split(".")
      |> Enum.map(&IntegerUtils.parse!/1)

    encode_identifier(:port, value, segments, context)
  end

  def encode_ir(%IR.ReferenceType{value: value}, context) do
    segments =
      value
      |> :erlang.ref_to_list()
      |> Enum.drop(5)
      |> List.delete_at(-1)
      |> to_string()
      |> String.split(".")
      |> Enum.map(&IntegerUtils.parse!/1)

    encode_identifier(:reference, value, segments, context)
  end

  def encode_ir(
        %IR.RemoteFunctionCall{
          module: module,
          function: function,
          args: args
        },
        context
      ) do
    encode_named_function_call(module, function, args, context)
  end

  def encode_ir(%IR.StringType{value: value}, _context) do
    encode_primitive_type(:bitstring, value, true)
  end

  # TODO: catch_clauses, else_clauses, after_block
  def encode_ir(%IR.Try{} = ir, context) do
    body_js = encode_closure(ir.body, context)
    rescue_clauses_js = encode_as_array(ir.rescue_clauses, context)

    "Interpreter.try(#{body_js}, #{rescue_clauses_js}, [], [], null, context)"
  end

  def encode_ir(%IR.TryRescueClause{} = ir, context) do
    variable_js =
      if ir.variable do
        encode_ir(ir.variable, %{context | pattern?: true})
      else
        "null"
      end

    modules_js = encode_as_array(ir.modules, context)
    body_js = encode_closure(ir.body, context)

    "{variable: #{variable_js}, modules: #{modules_js}, body: #{body_js}}"
  end

  def encode_ir(%IR.TupleType{data: data}, context) do
    data_js = encode_as_array(data, context)
    "Type.tuple(#{data_js})"
  end

  def encode_ir(%IR.Variable{name: name, version: version}, %{pattern?: true}) do
    var_name = encode_var_name(name, version)
    ~s/Type.variablePattern("#{var_name}")/
  end

  def encode_ir(%IR.Variable{name: name, version: version}, %{pattern?: false}) do
    encode_var(name, version)
  end

  # TODO: finish implementing
  def encode_ir(%IR.With{}, _context) do
    "Interpreter.with()"
  end

  @doc """
  Encodes Elixir term into JavaScript.
  If the term can be encoded into JavaScript then the result is in the shape of {:ok, js}.
  If the term can't be encoded into JavaScript then an error message is returned in the shape of {:error, message}.
  """
  @spec encode_term(any) :: {:ok, String.t()} | {:error, String.t()}
  def encode_term(term) do
    {:ok, encode_term!(term)}
  rescue
    e in ArgumentError ->
      {:error, e.message}
  end

  @doc """
  Encodes Elixir term into JavaScript, erroring out if the term can't be encoded into JavaScript.
  """
  @spec encode_term!(any) :: String.t()
  def encode_term!(term) do
    term
    |> IR.for_term!()
    |> encode_ir(%Context{})
  end

  @doc """
  Prints debug info for intercepted encode_ir/2 calls.
  """
  @spec debug(
          {module, atom, list(IR.t() | Context.t())},
          String.t() | %{__struct__: FunctionClauseError},
          integer
        ) :: :ok
  def debug({_module, _function, [ir, context] = _args}, result, _start_timestamp) do
    # credo:disable-for-lines:10 /Credo.Check.Refactor.IoPuts|Credo.Check.Warning.IoInspect/
    IO.puts("\nENCODE IR...............................\n")
    IO.puts("ir")
    IO.inspect(ir)
    IO.puts("")
    IO.puts("context")
    IO.inspect(context)
    IO.puts("")
    IO.puts("result")
    IO.inspect(result)
    IO.puts("\n........................................\n")
  end

  defp encode_as_array(data, context, encoder \\ &encode_ir/2) do
    data
    |> Enum.map_join(", ", &encoder.(&1, context))
    |> StringUtils.wrap("[", "]")
  end

  defp encode_as_string(value, wrap)

  defp encode_as_string(nil, false) do
    "nil"
  end

  defp encode_as_string(value, false) do
    value
    |> to_string()
    |> escape_non_printable_and_special_chars()
  end

  defp encode_as_string(value, true) do
    value
    |> encode_as_string(false)
    |> StringUtils.wrap("\"", "\"")
  end

  defp encode_bitstring_modifier({:size, size}, context) do
    "size: #{encode_ir(size, context)}"
  end

  defp encode_bitstring_modifier({:unit, unit}, _context) do
    "unit: #{unit}n"
  end

  defp encode_bitstring_modifier({name, value}, _context) do
    ~s(#{name}: "#{value}")
  end

  defp encode_bitstring_segment(value_str, modifiers, context) do
    modifiers_str =
      modifiers
      |> Enum.map_join(", ", &encode_bitstring_modifier(&1, context))
      |> StringUtils.wrap("{", "}")

    "Type.bitstringSegment(#{value_str}, #{modifiers_str})"
  end

  defp encode_bitstring_segments(segments, context) do
    Enum.map_join(segments, ", ", &encode_ir(&1, context))
  end

  defp encode_block_body(%IR.Block{expressions: exprs}, context) do
    exprs =
      if exprs == [] do
        [%IR.AtomType{value: nil}]
      else
        exprs
      end

    expr_count = Enum.count(exprs)

    body =
      exprs
      |> Enum.with_index()
      |> Enum.map_join("", fn {expr, idx} ->
        expr_js = encode_ir(expr, context)

        last_expr? = idx == expr_count - 1
        has_match_operator? = has_match_operator?(expr)

        encode_block_expr(expr_js, last_expr?, has_match_operator?)
      end)

    "{#{body}\n}"
  end

  defp encode_block_expr(expr_js, last_expr?, has_match_operator?)

  defp encode_block_expr(expr_js, true, true) do
    StringUtils.normalize_newlines("""

    globalThis.hologram.return = #{expr_js};
    Interpreter.updateVarsToMatchedValues(context);
    return globalThis.hologram.return;\
    """)
  end

  defp encode_block_expr(expr_js, true, false) do
    "\nreturn #{expr_js};"
  end

  defp encode_block_expr(expr_js, false, true) do
    StringUtils.normalize_newlines("""

    #{expr_js};
    Interpreter.updateVarsToMatchedValues(context);\
    """)
  end

  defp encode_block_expr(expr_js, false, false) do
    "\n#{expr_js};"
  end

  defp encode_closure(ir, context)

  defp encode_closure(nil, _context), do: "null"

  defp encode_closure(%IR.Block{} = ir, context) do
    "(context) => #{encode_block_body(ir, context)}"
  end

  defp encode_closure(ir, context) do
    "(context) => #{encode_ir(ir, context)}"
  end

  defp encode_dynamic_named_function_call(module, function, args, context) do
    module_js = encode_ir(module, context)
    function_js = encode_ir(function, context)
    args_js = encode_ir(args, context)

    "Interpreter.callNamedFunction(#{module_js}, #{function_js}, #{args_js}, context)"
  end

  defp encode_identifier(type, value, segments, context) do
    encoded_node = encode_as_string(node(value), true)

    integer_encoder = fn integer, _context -> to_string(integer) end
    encoded_segments = encode_as_array(segments, context, integer_encoder)

    "Type.#{type}(#{encoded_node}, #{encoded_segments})"
  end

  defp encode_named_function_call(%IR.AtomType{value: :erlang}, :andalso, [left, right], context) do
    left_js = encode_closure(left, context)
    right_js = encode_closure(right, context)

    "Erlang[\"andalso/2\"](#{left_js}, #{right_js}, context)"
  end

  defp encode_named_function_call(
         %IR.AtomType{value: :erlang},
         :apply,
         [module, function, args],
         context
       ) do
    encode_dynamic_named_function_call(module, function, args, context)
  end

  defp encode_named_function_call(%IR.AtomType{value: :erlang}, :orelse, [left, right], context) do
    left_js = encode_closure(left, context)
    right_js = encode_closure(right, context)

    "Erlang[\"orelse/2\"](#{left_js}, #{right_js}, context)"
  end

  defp encode_named_function_call(%IR.AtomType{} = module, function, args, context) do
    class = encode_as_class_name(module.value)
    arity = Enum.count(args)
    args_js = Enum.map_join(args, ", ", &encode_ir(&1, context))

    "#{class}[\"#{function}/#{arity}\"](#{args_js})"
  end

  defp encode_named_function_call(module_ir, function, args, context) do
    function_ir =
      if is_atom(function) do
        %IR.AtomType{value: function}
      else
        function
      end

    args_ir =
      if is_list(args) do
        %IR.ListType{data: args}
      else
        args
      end

    encode_dynamic_named_function_call(module_ir, function_ir, args_ir, context)
  end

  defp encode_primitive_type(type, value, as_string)

  defp encode_primitive_type(type, value, true) do
    value_str = encode_as_string(value, true)
    encode_primitive_type(type, value_str, false)
  end

  defp encode_primitive_type(type, value, false) do
    "Type.#{type}(#{value})"
  end

  defp encode_var(name, version) do
    var_name = encode_var_name(name, version)

    if String.match?(var_name, ~r/[^a-zA-Z0-9_]+/) do
      ~s'context.vars["#{var_name}"]'
    else
      "context.vars.#{var_name}"
    end
  end

  defp encode_var_name(name, nil) do
    encode_as_string(name, false)
  end

  defp encode_var_name(name, version) do
    encode_as_string(name, false) <> "_#{version}"
  end

  defp escape_non_printable_and_special_chars(str)

  defp escape_non_printable_and_special_chars("\\" <> rest) do
    "\\\\" <> escape_non_printable_and_special_chars(rest)
  end

  defp escape_non_printable_and_special_chars("\"" <> rest) do
    "\\\"" <> escape_non_printable_and_special_chars(rest)
  end

  defp escape_non_printable_and_special_chars("\a" <> rest) do
    "\\x07" <> escape_non_printable_and_special_chars(rest)
  end

  defp escape_non_printable_and_special_chars("\b" <> rest) do
    "\\b" <> escape_non_printable_and_special_chars(rest)
  end

  defp escape_non_printable_and_special_chars("\f" <> rest) do
    "\\f" <> escape_non_printable_and_special_chars(rest)
  end

  defp escape_non_printable_and_special_chars("\n" <> rest) do
    "\\n" <> escape_non_printable_and_special_chars(rest)
  end

  defp escape_non_printable_and_special_chars("\r" <> rest) do
    "\\r" <> escape_non_printable_and_special_chars(rest)
  end

  defp escape_non_printable_and_special_chars("\t" <> rest) do
    "\\t" <> escape_non_printable_and_special_chars(rest)
  end

  defp escape_non_printable_and_special_chars("\v" <> rest) do
    "\\v" <> escape_non_printable_and_special_chars(rest)
  end

  # Line separator character (LS)
  # (JavaScript editors have problems with this char)
  defp escape_non_printable_and_special_chars(<<8_232::utf8>> <> rest) do
    "\\u{2028}" <> escape_non_printable_and_special_chars(rest)
  end

  # Paragraph separator character (PS)
  # (JavaScript editors have problems with this char)
  defp escape_non_printable_and_special_chars(<<8_233::utf8>> <> rest) do
    "\\u{2029}" <> escape_non_printable_and_special_chars(rest)
  end

  defp escape_non_printable_and_special_chars(<<code::utf8, rest::binary>>) do
    char = <<code::utf8>>

    escaped_char =
      if String.printable?(char) do
        char
      else
        "\\u{#{Integer.to_string(code, 16)}}"
      end

    escaped_char <> escape_non_printable_and_special_chars(rest)
  end

  defp escape_non_printable_and_special_chars(<<char::integer, rest::binary>>) do
    # No need to pad with 0, because chars smaller that 16 will be encoded differently
    "\\x#{Integer.to_string(char, 16)}" <> escape_non_printable_and_special_chars(rest)
  end

  defp escape_non_printable_and_special_chars(""), do: ""

  defp extract_erlang_function_source_code(file_path, function, arity) do
    key = "#{function}/#{arity}"
    start_marker = "// Start #{key}"
    end_marker = "// End #{key}"

    regex =
      ~r/#{Regex.escape(start_marker)}[[:space:]]+"#{Regex.escape(key)}":[[:space:]]+(.+),[[:space:]]+#{Regex.escape(end_marker)}/s

    file_contents = File.read!(file_path)

    case Regex.run(regex, file_contents) do
      [_full_capture, source_code] -> source_code
      nil -> nil
    end
  end

  defp has_match_operator?(ir)

  defp has_match_operator?(%IR.MatchOperator{}), do: true

  defp has_match_operator?(ir) when is_list(ir) do
    Enum.any?(ir, &has_match_operator?/1)
  end

  defp has_match_operator?(ir) when is_tuple(ir) do
    ir
    |> Tuple.to_list()
    |> has_match_operator?()
  end

  defp has_match_operator?(%_struct{} = ir) do
    ir
    |> Map.from_struct()
    |> has_match_operator?()
  end

  defp has_match_operator?(ir) when is_map(ir) do
    Enum.any?(ir, fn {key, value} -> has_match_operator?(key) || has_match_operator?(value) end)
  end

  defp has_match_operator?(_ast), do: false
end
