defmodule BeamFile do
  @moduledoc """
  An interface to the BEAM file format.

  This module is mainly a wrapper around Erlangs `:beam_lib`.

  For more information see the Erlang documentation for the module
  [`beam_lib`](https://www.erlang.org/doc/man/beam_lib.html)

  Furthermore, different code representations can be generated from the file.
  - `BeamFile.abstract_code/1`
  - `BeamFile.byte_code/1`
  - `BeamFile.erl_code/1`
  - `BeamFile.elixir_code/2`
  - `BeamFile.elixir_quoted/1`

  To use the functions above with a module name the module must be compiled
  and loaded. The functions can also be used with the binary of a module.
  """

  alias BeamFile.DebugInfo
  alias BeamFile.Error

  @type info :: [
          file: Path.t() | binary(),
          module: module(),
          chunks: [{charlist(), non_neg_integer(), non_neg_integer()}]
        ]

  @type path :: charlist()

  @type beam :: path() | binary()

  @type input ::
          beam()
          | module()
          | {:module, module(), binary(), any()}
          | [{module(), binary()}]

  @type reason :: any()

  @typedoc """
  Chunk ID.

  ```
  'Abst'
  | 'Attr'
  | 'AtU8'
  | 'CInf'
  | 'Dbgi'
  | 'Dcos'
  | 'ExCk'
  | 'ExpT'
  | 'ImpT'
  | 'LocT'
  ```
  """
  @type chunk_id :: charlist()

  @type chunk_name ::
          :abstract_code
          | :atoms
          | :attributes
          | :compile_info
          | :debug_info
          | :docs
          | :elixir_checker
          | :exports
          | :imports
          | :indexed_imports
          | :labeled_exports
          | :labeled_locals
          | :locals

  @type chunk_ref :: chunk_name | chunk_id

  @chunk_ids [
    ~c"Abst",
    ~c"AtU8",
    ~c"Attr",
    ~c"CInf",
    ~c"Dbgi",
    ~c"Docs",
    ~c"ExCk",
    ~c"ExpT",
    ~c"ImpT",
    ~c"LocT"
  ]

  @chunk_names [
    :abstract_code,
    :atoms,
    :attributes,
    :compile_info,
    :debug_info,
    :docs,
    :elixir_checker,
    :exports,
    :imports,
    :indexed_imports,
    :labeled_exports,
    :labeled_locals,
    :locals
  ]

  @since_default Version.parse_requirement!(">= 0.0.0")

  @doc """
  Returns the `:abstract_code` chunk.

  ## Examples

      iex> BeamFile.abstract_code(BeamFile.Example)
      {
        :ok,
        [
          {:attribute, 1, :file, {~c"test/fixtures/example.ex", 1}},
          {:attribute, 1, :module, BeamFile.Example},
          {:attribute, 1, :compile, [:no_auto_import]},
          {:attribute, 1, :export, [__info__: 1, hello: 0]},
          {
            :attribute,
            1,
            :spec,
            {
              {:__info__, 1},
              [
                {
                  :type,
                  1,
                  :fun,
                  [
                    {
                      :type,
                      1,
                      :product,
                      [
                        {
                          :type,
                          1,
                          :union,
                          [
                            {:atom, 1, :attributes},
                            {:atom, 1, :compile},
                            {:atom, 1, :functions},
                            {:atom, 1, :macros},
                            {:atom, 1, :md5},
                            {:atom, 1, :exports_md5},
                            {:atom, 1, :module},
                            {:atom, 1, :deprecated},
                            {:atom, 1, :struct}
                          ]
                        }
                      ]
                    },
                    {:type, 1, :any, []}
                  ]
                }
              ]
            }
          },
          {
            :function,
            0,
            :__info__,
            1,
            [
              {:clause, 0, [{:atom, 0, :module}], [], [{:atom, 0, BeamFile.Example}]},
              {
                :clause,
                0,
                [{:atom, 0, :functions}],
                [],
                [{:cons, 0, {:tuple, 0, [{:atom, 0, :hello}, {:integer, 0, 0}]}, {nil, 0}}]
              },
              {:clause, 0, [{:atom, 0, :macros}], [], [nil: 0]},
              {:clause, 0, [{:atom, 0, :struct}], [], [{:atom, 0, nil}]},
              {
                :clause,
                0,
                [{:atom, 0, :exports_md5}],
                [],
                [
                  {
                    :bin,
                    0,
                    [
                      {
                        :bin_element,
                        0,
                        {:string, 0,
                          [166, 117, 1, 22, 146, 56, 30, 199, 203, 141, 158, 223, 3, 11, 225, 190]},
                        :default,
                        :default
                      }
                    ]
                  }
                ]
              },
              {
                :clause,
                0,
                [{:match, 0, {:var, 0, :Key}, {:atom, 0, :attributes}}],
                [],
                [
                  {
                    :call,
                    0,
                    {:remote, 0, {:atom, 0, :erlang}, {:atom, 0, :get_module_info}},
                    [{:atom, 0, BeamFile.Example}, {:var, 0, :Key}]
                  }
                ]
              },
              {
                :clause,
                0,
                [{:match, 0, {:var, 0, :Key}, {:atom, 0, :compile}}],
                [],
                [
                  {
                    :call,
                    0,
                    {:remote, 0, {:atom, 0, :erlang}, {:atom, 0, :get_module_info}},
                    [{:atom, 0, BeamFile.Example}, {:var, 0, :Key}]
                  }
                ]
              },
              {
                :clause,
                0,
                [{:match, 0, {:var, 0, :Key}, {:atom, 0, :md5}}],
                [],
                [
                  {
                    :call,
                    0,
                    {:remote, 0, {:atom, 0, :erlang}, {:atom, 0, :get_module_info}},
                    [{:atom, 0, BeamFile.Example}, {:var, 0, :Key}]
                  }
                ]
              },
              {:clause, 0, [{:atom, 0, :deprecated}], [], [nil: 0]}
            ]
          },
          {:function, {2, 7}, :hello, 0, [{:clause, {2, 7}, [], [], [{:atom, {2, 7}, :world}]}]}
        ]
      }
  """
  @spec abstract_code(input()) :: {:ok, term()} | {:error, any()}
  def abstract_code(input) do
    with {:ok, {:raw_abstract_v1, abstract_code}} <- chunk(input, :abstract_code) do
      {:ok, abstract_code}
    end
  end

  @doc """
  Same as `abstract_code/1` but raises `BeamFile.Error`
  """
  @spec abstract_code!(input()) :: term()
  def abstract_code!(input) do
    case abstract_code(input) do
      {:ok, abstract_code} ->
        abstract_code

      {:error, reason} ->
        raise Error, """
        Abstract code for #{inspect(input, binaries: :as_binaries)} not available, \
        reason: #{inspect(reason, binaries: :as_binaries)}\
        """
    end
  end

  @doc """
  Returns chunk data for all chunks.

  The `type` argument forces the use of `:ids` or `:names`, defaults to `:names`.

  ## Examples

      iex> {:ok, chunks} = BeamFile.all_chunks(BeamFile.Example, :names)
      iex> chunks |> Map.keys() |> Enum.sort()
      [
        :abstract_code,
        :atoms,
        :attributes,
        :compile_info,
        :debug_info,
        :docs,
        :elixir_checker,
        :exports,
        :imports,
        :indexed_imports,
        :labeled_exports,
        :labeled_locals,
        :locals
      ]
      iex> {:docs_v1, 1, :elixir, "text/markdown", :none, _meta, docs} = Map.get(chunks, :docs)
      iex> docs
      [{{:function, :hello, 0}, 2, ["hello()"], :none, %{source_annos: [{2, 7}]}}]

      iex> {:ok, chunks} = BeamFile.all_chunks(BeamFile.Example, :ids)
      iex> chunks |> Map.keys() |> Enum.sort()
      [~c"Abst", ~c"AtU8", ~c"Attr", ~c"CInf", ~c"Dbgi", ~c"Docs", ~c"ExCk", ~c"ExpT", ~c"ImpT", ~c"LocT"]
      iex> chunks |> Map.get(~c"Docs") |> is_binary()
      true
  """
  @spec all_chunks(input(), type :: :names | :ids) :: {:ok, map()} | {:error, any()}
  def all_chunks(input, type \\ :names) when type in [:ids, :names] do
    with {:ok, input} <- binary(input) do
      chunks =
        case type do
          :names -> @chunk_names
          :ids -> @chunk_ids
        end

      fetch_chunks(input, chunks, type)
    end
  end

  @doc """
  Same as `all_chunks/` but raises `BeamFile.Error`
  """
  @spec all_chunks!(input(), type :: :names | :ids) :: map
  def all_chunks!(input, type \\ :names) do
    case all_chunks(input, type) do
      {:ok, chunks} ->
        chunks

      {:error, reason} ->
        raise Error, """
        Chunks for #{inspect(input, binaries: :as_binaries)} not available, \
        reason: #{inspect(reason, binaries: :as_binaries)}\
        """
    end
  end

  @doc """
  Returns the byte code for the BEAM file.

  ## Examples

      iex> {:ok, byte_code} = BeamFile.byte_code(BeamFile.Example)
      iex> byte_code |> Tuple.to_list() |> Enum.take(3)
      [
        :beam_file,
        BeamFile.Example,
        [{:__info__, 1, 2}, {:hello, 0, 11}, {:module_info, 0, 13}, {:module_info, 1, 15}]
      ]
  """
  @spec byte_code(input()) :: {:ok, term()} | {:error, any()}
  def byte_code(input) do
    with {:ok, data} <- binary(input) do
      case :beam_disasm.file(data) do
        {:error, mod, reason} -> {:error, {mod, reason}}
        byte_code -> {:ok, byte_code}
      end
    end
  end

  @doc """
  Same as `byte_code/1` but raises `BeamFile.Error`
  """
  @spec byte_code!(input()) :: term()
  def byte_code!(input) do
    case byte_code(input) do
      {:ok, byte_code} ->
        byte_code

      {:error, reason} ->
        raise Error, """
        Byte code for #{inspect(input, binaries: :as_binaries)} not available, \
        reason: #{inspect(reason, binaries: :as_binaries)}\
        """
    end
  end

  @doc """
  Returns infos for the given chunk reference.

  ## Examples

      iex> BeamFile.chunk(BeamFile.Example, :exports)
      {:ok, [__info__: 1, hello: 0, module_info: 0, module_info: 1]}

      iex> {:ok, chunk} = BeamFile.chunk(BeamFile.Example, ~c"Dbgi")
      iex> is_binary(chunk)
      true
  """
  @spec chunk(input(), chunk_ref()) :: {:ok, term()} | {:error, any()}
  def chunk(input, chunk) when chunk in @chunk_ids or chunk in @chunk_names do
    with {:ok, input} <- binary(input) do
      type =
        cond do
          chunk in @chunk_names -> :names
          chunk in @chunk_ids -> :ids
        end

      with {:ok, data} <- fetch_chunks(input, [chunk], type) do
        [{_key, value}] = Map.to_list(data)
        {:ok, value}
      end
    end
  end

  @doc """
  Same as `chunk/2` but raises `BeamFile.Error`
  """
  @spec chunk!(input(), chunk_ref()) :: term
  def chunk!(input, chunk) do
    case chunk(input, chunk) do
      {:ok, data} ->
        data

      {:error, reason} ->
        raise Error, """
        Chunk #{inspect(chunk)} for #{inspect(input, binaries: :as_binaries)} not available, \
        reason: #{inspect(reason, binaries: :as_binaries)}\
        """
    end
  end

  @doc """
  Returns the infos from the  `:debug_info` chunk.

  Examples:

      iex> {:ok, info} = BeamFile.debug_info(BeamFile.Example)
      iex> info |> Map.get(:definitions) |> hd() |> elem(0)
      {:hello, 0}
      iex> Map.get(info, :relative_file)
      "test/fixtures/example.ex"

  """
  @spec debug_info(input()) :: {:ok, term()} | {:error, any}
  def debug_info(input) do
    with {:ok, {:debug_info_v1, backend, data}} <- chunk(input, :debug_info) do
      case data do
        {:elixir_v1, debug_info, _meta} ->
          backend.debug_info(:elixir_v1, debug_info.module, data, [])

        :none ->
          {:error, :no_debug_info}
      end
    end
  end

  @doc """
  Same as `debug_info/1` but raises `BeamFile.Error`
  """
  @spec debug_info!(input()) :: term()
  def debug_info!(input) do
    case debug_info(input) do
      {:ok, debug_info} ->
        debug_info

      {:error, reason} ->
        raise Error, """
        Debug info for #{inspect(input, binaries: :as_binaries)} not available, \
        reason: #{inspect(reason, binaries: :as_binaries)}\
        """
    end
  end

  @doc """
  Returns the infos from the `:docs` chunk.

  ## Options

    * `format` - the output format. The format `:info` reduces the output to the
      type and the infos `:since`, `:hidden` and `:deprecated`. The format `since`
      reduces the output to the type and the info `:since`.

    * `hidden` - indicates whether only hidden or none hidden items are returned.
      Without the option hidden and none hidden items are returned.

    * `deprecated` - indicates wheter only deprecated or none deprecated items are
      returned. Without the option deprecated and none deprecated items are returned.

    * `since` - expected a version requirement. With `since`, the function returns
      all items whose since attribute matches `since`. When `since` does not match
      the module attribute since and not any item in the module matches, the tuple
      `{:ok, nil}` is returned.

  ## Examples

      iex> {:ok, {:none, _meta, docs}} = BeamFile.docs(BeamFile.Example)
      iex> docs
      [{{:function, :hello, 0}, 2, ["hello()"], :none, %{source_annos: [{2, 7}]}}]

  Examples with options:

      iex> BeamFile.docs(Float, format: :info, deprecated: true)
      {:ok,
       {[
          {{:function, :to_char_list, 1}, [since: nil, hidden: true, deprecated: true]},
          {{:function, :to_char_list, 2}, [since: nil, hidden: true, deprecated: true]},
          {{:function, :to_string, 2}, [since: nil, hidden: true, deprecated: true]}
        ], [since: nil, hidden: false, deprecated: false]}}

      iex> BeamFile.docs(Date, format: :since, since: "~> 1.12")
      {:ok,
       {[
          {{:function, :after?, 2}, [since: "1.15.0"]},
          {{:function, :before?, 2}, [since: "1.15.0"]},
          {{:function, :range, 3}, [since: "1.12.0"]},
          {{:function, :shift, 2}, [since: "1.17.0"]}
        ], [since: nil]}}
  """
  @spec docs(input(), options :: keyword()) :: {:ok, term()} | {:error, any()}
  def docs(input, options \\ []) do
    with {:ok, {:docs_v1, _, :elixir, "text/markdown", doc, meta, docs}} <- chunk(input, :docs) do
      {:ok, docs(doc, meta, docs, options)}
    end
  end

  defp docs(doc, meta, docs, []) do
    {doc, meta, docs}
  end

  defp docs(doc, meta, docs, options) do
    since = Keyword.get(options, :since, @since_default)
    deprecated = Keyword.get(options, :deprecated)
    hidden = Keyword.get(options, :hidden, true)
    format = Keyword.get(options, :format)

    docs =
      docs
      |> docs_since(since)
      |> docs_hidden(hidden)
      |> docs_deprecated(deprecated)

    if not Enum.empty?(docs) or since_match?(meta, since) do
      docs_format(doc, meta, docs, format)
    else
      nil
    end
  end

  defp docs_since(docs, @since_default), do: docs

  defp docs_since(docs, since) do
    Enum.filter(docs, fn doc -> doc |> elem(4) |> since_match?(since) end)
  end

  defp docs_hidden(docs, hidden) do
    Enum.reject(docs, fn doc ->
      elem(doc, 3) == :hidden && !hidden
    end)
  end

  defp docs_deprecated(docs, nil), do: docs

  defp docs_deprecated(docs, deprecated) do
    Enum.filter(docs, fn doc ->
      meta = elem(doc, 4)
      deprecated?(meta) == deprecated
    end)
  end

  defp docs_format(doc, meta, docs, nil) do
    {doc, meta, docs}
  end

  defp docs_format(doc, meta, docs, format) do
    docs =
      Enum.map(docs, fn {info, _line, _name, doc, meta} ->
        {info, info(format, doc, meta)}
      end)

    {docs, info(format, doc, meta)}
  end

  defp info(:info, doc, meta) do
    [since: Map.get(meta, :since), hidden: doc == :hidden, deprecated: deprecated?(meta)]
  end

  defp info(:since, _doc, meta) do
    [since: Map.get(meta, :since)]
  end

  defp deprecated?(meta) do
    meta |> Map.get(:deprecated) |> is_binary()
  end

  defp since_match?(meta, since) when is_map(meta) do
    meta |> Map.get(:since, "0.0.0") |> normalize_version() |> Version.match?(since)
  end

  defp normalize_version(version) do
    if Regex.match?(~r/^\d+\.\d+$/, version), do: "#{version}.0", else: version
  end

  @doc """
  Same as `docs/2` but raises `BeamFile.Error`
  """
  @spec docs!(input(), options: keyword()) :: term()
  def docs!(input, options \\ []) do
    case docs(input, options) do
      {:ok, docs} ->
        docs

      {:error, reason} ->
        raise Error, """
        Docs for #{inspect(input, binaries: :as_binaries)} not available, \
        reason: #{inspect(reason, binaries: :as_binaries)}\
        """
    end
  end

  @doc ~S'''
  Returns elixir code recreated from the `debug_info` chunk.

  The recreated code comes with resolved macros and references.
  For now, types and specs will not be recreated.

  Options:
  `:docs`: With `docs: true` the docs will be created.

  ## Examples

      iex> BeamFile.elixir_code(BeamFile.Example)
      {
        :ok,
        """
        defmodule Elixir.BeamFile.Example do
          def hello do
            :world
          end
        end\
        """
      }
  '''
  @spec elixir_code(input(), opts :: keyword()) :: {:ok, String.t()} | {:error, any}
  def elixir_code(input, opts \\ []) do
    with {:ok, debug_info} <- debug_info(input),
         {:ok, docs} <- maybe_docs(input, opts) do
      code =
        if docs do
          DebugInfo.code(debug_info, docs)
        else
          debug_info |> DebugInfo.ast(:code) |> Macro.to_string()
        end

      {:ok, code}
    end
  end

  defp maybe_docs(input, opts) do
    if Keyword.get(opts, :docs, false), do: docs(input), else: {:ok, nil}
  end

  @doc """
  Same as `elixir_code/1` but raises `BeamFile.Error`
  """
  @spec elixir_code!(input(), opts :: keyword()) :: String.t()
  def elixir_code!(input, opts \\ []) do
    case elixir_code(input, opts) do
      {:ok, code} ->
        code

      {:error, reason} ->
        raise Error, """
        Elixir code for #{inspect(input, binaries: :as_binaries)} not available, \
        reason: #{inspect(reason, binaries: :as_binaries)}\
        """
    end
  end

  @doc """
  Returns the extended Elixir AST.
  """
  @spec elixir_quoted(input()) :: {:ok, Macro.t()} | {:error, any()}
  def elixir_quoted(input) do
    with {:ok, debug_info} <- debug_info(input) do
      {:ok, DebugInfo.ast(debug_info)}
    end
  end

  @doc """
  Same as `elixir_quoted/1` but raises `BeamFile.Error`
  """
  @spec elixir_quoted!(input()) :: Macro.t()
  def elixir_quoted!(input) do
    case elixir_quoted(input) do
      {:ok, ast} ->
        ast

      {:error, reason} ->
        raise Error, """
        Elixir AST for #{inspect(input, binaries: :as_binaries)} not available, \
        reason: #{inspect(reason, binaries: :as_binaries)}\
        """
    end
  end

  @doc """
  Returns the Erlang code for the BEAM file.

  ## Examples

      iex> {:ok, code} = BeamFile.erl_code(BeamFile.Example)
      iex> code =~ "-module('Elixir.BeamFile.Example')"
      true
  """
  @spec erl_code(input()) :: {:ok, String.t()} | {:error, any()}
  def erl_code(input) do
    with {:ok, abstract_code} <- abstract_code(input) do
      code =
        abstract_code
        |> :erl_syntax.form_list()
        |> :erl_prettypr.format()
        |> to_string()

      {:ok, code}
    end
  end

  @doc """
  Same as `erl_code/1` but raises `BeamFile.Error`
  """
  @spec erl_code!(input()) :: String.t()
  def erl_code!(input) do
    case erl_code(input) do
      {:ok, code} ->
        code

      {:error, reason} ->
        raise Error, """
        Erlang code for #{inspect(input, binaries: :as_binaries)} not available, \
        reason: #{inspect(reason, binaries: :as_binaries)}\
        """
    end
  end

  @doc """
  Returns `true` if a BEAM file for the given `module` exists.

  ## Examples

      iex> BeamFile.exists?(BeamFile.Example)
      true
      iex> BeamFile.exists?(Physics.TOE)
      false
  """
  @spec exists?(module()) :: boolean()
  def exists?(module) do
    case :code.which(module) do
      :non_existing -> false
      _path -> true
    end
  end

  @doc """
  Returns a keyword list containing some information about a BEAM file.

  - `:file`: The name of the BEAM file, or the binary from which the information
    was extracted.
  - `:module`: The name of the module.
  - `:chunks`: For each chunk, the identifier and the position and size of the
    chunk data, in bytes.

  ## Examples

      iex> {:ok, info} = BeamFile.info(BeamFile.Example)
      iex> info[:module]
      BeamFile.Example
      iex> info[:chunks]
      ...> |> Enum.map(fn {id, _pos, _size} -> id end)
      ...> |> Enum.sort()
      [
        ~c"AtU8",
        ~c"Attr",
        ~c"CInf",
        ~c"Code",
        ~c"Dbgi",
        ~c"Docs",
        ~c"ExCk",
        ~c"ExpT",
        ~c"ImpT",
        ~c"Line",
        ~c"LitT",
        ~c"LocT",
        ~c"StrT",
        ~c"Type"
      ]
  """
  @spec info(input()) :: {:ok, info()} | {:error, reason()}
  def info(input) do
    with {:ok, input} <- beam(input),
         {:ok, info} <- do_info(input) do
      {:ok, info_file(info, input)}
    end
  end

  defp info_file(info, input) when is_list(input) do
    Keyword.put(info, :file, IO.chardata_to_string(input))
  end

  defp info_file(info, _input), do: info

  defp do_info(input) do
    case :beam_lib.info(input) do
      {:error, :beam_lib, {:file_error, _path, reason}} -> {:error, reason}
      {:error, :beam_lib, reason} -> {:error, reason}
      info -> {:ok, info}
    end
  end

  @doc """
  Returns the absolute filename for the `module`.

  If the module cannot be found, `{:error, :non_existing}` is returned.

  If the module is preloaded, `{:error, :preloaded}` is returned.

  If the module is Cover-compiled, `{:error, :cover_compiled}` is returned.

  ## Examples

      iex> {:ok, path} = BeamFile.which(BeamFile.Example)
      iex> path =~ "/_build/test/lib/beam_file/ebin/Elixir.BeamFile.Example.beam"
  """
  @spec which(module()) ::
          {:ok, Path.t()}
          | {:error, :non_existing | :preloaded | :cover_compiled}
  def which(module) when is_atom(module) do
    with {:ok, path} <- path(module) do
      {:ok, path |> to_string() |> Path.relative_to_cwd()}
    end
  end

  @doc """
  Same as `which/1` but raises `BeamFile.Error`
  """
  @spec which!(module()) :: Path.t()
  def which!(module) when is_atom(module) do
    case which(module) do
      {:ok, path} ->
        path

      {:error, reason} ->
        raise Error, "Path for #{module} not available, reason: #{inspect(reason)}."
    end
  end

  @doc """
  Returns the `binary` for the given input.
  """
  @spec binary(input()) ::
          {:ok, binary()}
          | {:error, File.posix()}
          | {:error, :non_existing | :preloaded | :cover_compiled}
  def binary(input) do
    with {:ok, data} <- beam(input) do
      if is_list(data) do
        data |> List.to_string() |> File.read()
      else
        {:ok, data}
      end
    end
  end

  @doc """
  Same as `binary/1` but raises `BeamFile.Error`
  """
  @spec binary!(input()) :: binary()
  def binary!(input) do
    case binary(input) do
      {:ok, binary} ->
        binary

      {:error, reason} ->
        raise Error, """
        Can not fetch #{inspect(input, binaries: :as_binaries)}, \
        reason: #{inspect(reason, binaries: :as_binaries)}\
        """
    end
  end

  @doc false
  @deprecated "use binary/1 instead"
  def read(input), do: binary(input)

  @doc false
  @deprecated "use binary!/1 instead"
  def read!(input), do: binary!(input)

  defp fetch_chunks(input, chunks, type) when type in [:ids, :names] do
    chunks = Enum.map(chunks, &to_erl_chunk/1)

    case :beam_lib.chunks(input, chunks, [:allow_missing_chunks]) do
      {:ok, {_module, data}} ->
        data = Enum.into(data, %{}, fn item -> to_elixir_data(item, type) end)
        {:ok, data}

      {:error, :beam_lib, reason} ->
        {:error, reason}
    end
  end

  # Some chunks are not available via a chunk-name. For this chunks we need the
  # chunk-id.
  defp to_erl_chunk(:docs), do: ~c"Docs"

  defp to_erl_chunk(:elixir_checker), do: ~c"ExCk"

  defp to_erl_chunk(chunk), do: chunk

  # For some chunks we need a transformation.
  defp to_elixir_data({~c"Docs", :missing_chunk}, :names), do: {:docs, :missing_chunk}

  defp to_elixir_data({~c"Docs", data}, :names), do: {:docs, :erlang.binary_to_term(data)}

  defp to_elixir_data({~c"ExCk", data}, :names),
    do: {:elixir_checker, :erlang.binary_to_term(data)}

  defp to_elixir_data({~c"Docs", data}, :ids), do: {~c"Docs", data}

  defp to_elixir_data({~c"ExCk", data}, :ids), do: {~c"ExCk", data}

  defp to_elixir_data(item, _type), do: item

  defp path(module) when is_atom(module) do
    case :code.which(module) do
      [] -> {:error, :non_existing}
      [_ | _] = path -> {:ok, path}
      error -> {:error, error}
    end
  end

  defp beam(input) when is_atom(input), do: path(input)
  defp beam({:module, module, binary, _context}) when is_atom(module), do: {:ok, binary}
  defp beam(input) when is_list(input), do: chardata(input)
  defp beam(input) when is_binary(input), do: {:ok, input}
  defp beam(_input), do: {:error, :invalid_input}

  defp chardata(input) do
    _string = IO.chardata_to_string(input)
    {:ok, input}
  rescue
    _error -> {:error, :invalid_input}
  end
end
