defmodule Mix.Tasks.Beam.File do
  @shortdoc "Prints the decompiled source for a module."

  @moduledoc """
  #{@shortdoc}

  ```shell
  mix beam.file module switch
  ```
  The argument `module` specifies the `module` to decompile. The `switch` can be
  one of `--elixir`, `--erlang`, or `--byte-code`.

  Decompiling with `--elixir` returns the expanded Elixir code.
  """

  use Mix.Task

  @opts strict: [elixir: :boolean, erlang: :boolean, byte_code: :boolean]
  @error ~s|Could not invoke task "beam.file":|

  @impl Mix.Task
  def run(opts) do
    opts
    |> OptionParser.parse!(@opts)
    |> validate()
    |> output()
  end

  defp output({[{type, true}], [module]}) do
    Mix.Task.run("compile")

    module = Module.concat([module])

    module
    |> code(type)
    |> check()
    |> IO.puts()
  end

  defp check({:ok, code}), do: code

  defp check({:error, error}) do
    Mix.raise("BeamFileError: #{to_string(error)}")
  end

  defp code(module, :elixir), do: BeamFile.elixir_code(module)

  defp code(module, :erlang), do: BeamFile.erl_code(module)

  defp code(module, :byte_code) do
    with {:ok, byte_code} <- BeamFile.byte_code(module) do
      {:ok, inspect(byte_code, pretty: true, limit: :infinity)}
    end
  end

  defp validate({[], _args}) do
    Mix.raise("""
    #{@error}
    Missing one of the switches `--elixir`, `--erlang`, or `--byte-code`.
    """)
  end

  defp validate({_opts, []}) do
    Mix.raise("""
    #{@error}
    Missing a moudle name.
    """)
  end

  defp validate({opts, args}) when length(opts) > 1 or length(args) > 1 do
    Mix.raise("""
    #{@error}
    Too many arguments.
    """)
  end

  defp validate(opts), do: opts
end
