defmodule Credo.Code.Block do
  @moduledoc """
  This module provides helper functions to analyse blocks, e.g. the block taken
  by the `if` macro.
  """

  @doc """
  Returns the do: block of a given AST node.
  """
  def all_blocks_for!(ast) do
    [
      do_block_for!(ast),
      else_block_for!(ast),
      rescue_block_for!(ast),
      after_block_for!(ast)
    ]
  end

  @doc """
  Returns true if the given `ast` has a do block.
  """
  def do_block?(ast) do
    case do_block_for(ast) do
      {:ok, _block} ->
        true

      nil ->
        false
    end
  end

  @doc """
  Returns the do: block of a given AST node.
  """
  def do_block_for!(ast) do
    case do_block_for(ast) do
      {:ok, block} ->
        block

      nil ->
        nil
    end
  end

  @doc """
  Returns a tuple {:ok, do_block} or nil for a given AST node.
  """
  def do_block_for({_atom, _meta, arguments}) when is_list(arguments) do
    do_block_for(arguments)
  end

  def do_block_for(do: block) do
    {:ok, block}
  end

  def do_block_for(do: do_block, else: _else_block) do
    {:ok, do_block}
  end

  def do_block_for(arguments) when is_list(arguments) do
    Enum.find_value(arguments, &find_keyword(&1, :do))
  end

  def do_block_for(_) do
    nil
  end

  @doc """
  Returns true if the given `ast` has an else block.
  """
  def else_block?(ast) do
    case else_block_for(ast) do
      {:ok, _block} ->
        true

      nil ->
        false
    end
  end

  @doc """
  Returns the `else` block of a given AST node.
  """
  def else_block_for!(ast) do
    case else_block_for(ast) do
      {:ok, block} ->
        block

      nil ->
        nil
    end
  end

  @doc """
  Returns a tuple {:ok, else_block} or nil for a given AST node.
  """
  def else_block_for({_atom, _meta, arguments}) when is_list(arguments) do
    else_block_for(arguments)
  end

  def else_block_for(do: _do_block, else: else_block) do
    {:ok, else_block}
  end

  def else_block_for(arguments) when is_list(arguments) do
    Enum.find_value(arguments, &find_keyword(&1, :else))
  end

  def else_block_for(_) do
    nil
  end

  @doc """
  Returns true if the given `ast` has an rescue block.
  """
  def rescue_block?(ast) do
    case rescue_block_for(ast) do
      {:ok, _block} ->
        true

      nil ->
        false
    end
  end

  @doc """
  Returns the rescue: block of a given AST node.
  """
  def rescue_block_for!(ast) do
    case rescue_block_for(ast) do
      {:ok, block} ->
        block

      nil ->
        nil
    end
  end

  @doc """
  Returns a tuple {:ok, rescue_block} or nil for a given AST node.
  """
  def rescue_block_for({_atom, _meta, arguments}) when is_list(arguments) do
    rescue_block_for(arguments)
  end

  def rescue_block_for(do: _do_block, rescue: rescue_block) do
    {:ok, rescue_block}
  end

  def rescue_block_for(arguments) when is_list(arguments) do
    Enum.find_value(arguments, &find_keyword(&1, :rescue))
  end

  def rescue_block_for(_) do
    nil
  end

  @doc """
  Returns true if the given `ast` has an catch block.
  """
  def catch_block?(ast) do
    case catch_block_for(ast) do
      {:ok, _block} ->
        true

      nil ->
        false
    end
  end

  @doc """
  Returns the catch: block of a given AST node.
  """
  def catch_block_for!(ast) do
    case catch_block_for(ast) do
      {:ok, block} ->
        block

      nil ->
        nil
    end
  end

  @doc """
  Returns a tuple {:ok, catch_block} or nil for a given AST node.
  """
  def catch_block_for({_atom, _meta, arguments}) when is_list(arguments) do
    catch_block_for(arguments)
  end

  def catch_block_for(do: _do_block, catch: catch_block) do
    {:ok, catch_block}
  end

  def catch_block_for(arguments) when is_list(arguments) do
    Enum.find_value(arguments, &find_keyword(&1, :catch))
  end

  def catch_block_for(_) do
    nil
  end

  @doc """
  Returns true if the given `ast` has an after block.
  """
  def after_block?(ast) do
    case after_block_for(ast) do
      {:ok, _block} ->
        true

      nil ->
        false
    end
  end

  @doc """
  Returns the after: block of a given AST node.
  """
  def after_block_for!(ast) do
    case after_block_for(ast) do
      {:ok, block} ->
        block

      nil ->
        nil
    end
  end

  @doc """
  Returns a tuple {:ok, after_block} or nil for a given AST node.
  """
  def after_block_for({_atom, _meta, arguments}) when is_list(arguments) do
    after_block_for(arguments)
  end

  def after_block_for(do: _do_block, after: after_block) do
    {:ok, after_block}
  end

  def after_block_for(arguments) when is_list(arguments) do
    Enum.find_value(arguments, &find_keyword(&1, :after))
  end

  def after_block_for(_) do
    nil
  end

  defp find_keyword(list, keyword) when is_list(list) do
    if Keyword.has_key?(list, keyword) do
      {:ok, list[keyword]}
    else
      nil
    end
  end

  defp find_keyword(_, _) do
    nil
  end

  @doc """
  Returns the children of the `do` block of the given AST node.
  """
  def calls_in_do_block({_op, _meta, arguments}) do
    arguments
    |> do_block_for!
    |> instructions_for
  end

  def calls_in_do_block(arg) do
    arg
    |> do_block_for!
    |> instructions_for
  end

  @doc """
  Returns the children of the `rescue` block of the given AST node.
  """
  def calls_in_rescue_block({_op, _meta, arguments}) do
    arguments
    |> rescue_block_for!
    |> instructions_for
  end

  def calls_in_rescue_block(arg) do
    arg
    |> rescue_block_for!
    |> instructions_for
  end

  @doc """
  Returns the children of the `catch` block of the given AST node.
  """
  def calls_in_catch_block({_op, _meta, arguments}) do
    arguments
    |> catch_block_for!
    |> instructions_for
  end

  def calls_in_catch_block(arg) do
    arg
    |> catch_block_for!
    |> instructions_for
  end

  defp instructions_for({:__block__, _meta, calls}), do: calls

  defp instructions_for(v)
       when is_atom(v) or is_tuple(v) or is_binary(v) or is_float(v) or is_integer(v),
       do: List.wrap(v)

  defp instructions_for(v) when is_list(v), do: [v]
end
