defmodule Phoenix.LiveDashboard.SystemInfo do
  # Helpers for fetching and formatting system info.
  @moduledoc false

  # These structs are not loaded into remote nodes, but they can be used as structs
  # because they are expanded at compile-time
  defmodule ProcessDetails do
    @moduledoc false
    defstruct [:pid, :name_or_initial_call, :initial_call]
  end

  defmodule PortDetails do
    @moduledoc false
    defstruct [:port, :description]
  end

  def node_capabilities(node, requirements) do
    case :erpc.call(node, :code, :is_loaded, [__MODULE__]) do
      {:file, _} ->
        maybe_replace(node, fetch_capabilities(node, requirements), requirements)

      false ->
        load(node, requirements)
    end
  end

  defp maybe_replace(node, capabilities, requirements) do
    if not capabilities.dashboard_running? and
         capabilities.system_info != __MODULE__.__info__(:md5) do
      load(node, requirements)
    else
      capabilities
    end
  end

  defp load(node, requirements) do
    {_module, binary, filename} = :code.get_object_code(__MODULE__)
    :erpc.call(node, :code, :load_binary, [__MODULE__, filename, binary])
    fetch_capabilities(node, requirements)
  end

  ## Fetchers

  def fetch_processes(node, search, sort_by, sort_dir, limit, prev_reductions \\ nil) do
    search = search && String.downcase(search)
    args = [search, sort_by, sort_dir, limit, prev_reductions]
    :erpc.call(node, __MODULE__, :processes_callback, args)
  end

  def fetch_ets(node, search, sort_by, sort_dir, limit) do
    search = search && String.downcase(search)
    :erpc.call(node, __MODULE__, :ets_callback, [search, sort_by, sort_dir, limit])
  end

  def fetch_sockets(node, search, sort_by, sort_dir, limit) do
    search = search && String.downcase(search)
    :erpc.call(node, __MODULE__, :sockets_callback, [search, sort_by, sort_dir, limit])
  end

  def fetch_socket_info(port) do
    :erpc.call(node(port), __MODULE__, :socket_info_callback, [port])
  end

  def fetch_process_info(pid) do
    :erpc.call(node(pid), __MODULE__, :process_info_callback, [pid])
  end

  def fetch_ports(node, search, sort_by, sort_dir, limit) do
    search = search && String.downcase(search)
    :erpc.call(node, __MODULE__, :ports_callback, [search, sort_by, sort_dir, limit])
  end

  def fetch_applications(node, search, sort_by, sort_dir, limit) do
    :erpc.call(node, __MODULE__, :applications_info_callback, [search, sort_by, sort_dir, limit])
  end

  def fetch_port_info(port) do
    :erpc.call(node(port), __MODULE__, :port_info_callback, [port])
  end

  def fetch_ets_info(node, ref) do
    :erpc.call(node, __MODULE__, :ets_info_callback, [ref])
  end

  def fetch_system_info(node, keys, app) do
    :erpc.call(node, __MODULE__, :info_callback, [keys, app])
  end

  def fetch_system_usage(node) do
    :erpc.call(node, __MODULE__, :usage_callback, [])
  end

  def fetch_os_mon_info(node) do
    :erpc.call(node, __MODULE__, :os_mon_callback, [])
  end

  def fetch_capabilities(node, requirements) do
    :erpc.call(node, __MODULE__, :capabilities_callback, [requirements])
  end

  def fetch_app_tree(node, application) do
    :erpc.call(node, __MODULE__, :app_tree_callback, [application])
  end

  def fetch_memory_allocators(node, max_carrier_sizes) do
    :erpc.call(node, __MODULE__, :memory_allocators_callback, [max_carrier_sizes])
  end

  ## System callbacks

  @doc false
  def info_callback(keys, app) do
    %{
      system_info: %{
        banner: :erlang.system_info(:system_version),
        elixir_version: System.version(),
        phoenix_version: Application.spec(:phoenix, :vsn) || "None",
        app_version: Application.spec(app, :vsn) || "None",
        system_architecture: :erlang.system_info(:system_architecture)
      },
      system_limits: %{
        atoms: :erlang.system_info(:atom_limit),
        ports: :erlang.system_info(:port_limit),
        processes: :erlang.system_info(:process_limit)
      },
      system_usage: usage_callback(),
      environment: env_info_callback(keys)
    }
  end

  @doc false
  def usage_callback do
    %{
      atoms: :erlang.system_info(:atom_count),
      ports: :erlang.system_info(:port_count),
      processes: :erlang.system_info(:process_count),
      io: io(),
      uptime: :erlang.statistics(:wall_clock) |> elem(0),
      memory: memory(),
      total_run_queue: :erlang.statistics(:total_run_queue_lengths_all),
      cpu_run_queue: :erlang.statistics(:total_run_queue_lengths)
    }
  end

  @doc false
  def capabilities_callback(requirements) do
    %{
      system_info: __MODULE__.__info__(:md5),
      dashboard_running?: is_pid(Process.whereis(Phoenix.LiveDashboard.DynamicSupervisor)),
      applications: capabilities_callback_applications(requirements),
      modules: capabilities_callback_modules(requirements),
      processes: capabilities_callback_processes(requirements)
    }
  end

  defp capabilities_callback_applications(requirements) do
    for {:application, app} <- requirements, Application.get_application(app), do: app
  end

  defp capabilities_callback_modules(requirements) do
    for {:module, mod} <- requirements, Code.ensure_loaded?(mod), do: mod
  end

  defp capabilities_callback_processes(requirements) do
    for {:process, process} <- requirements, Process.whereis(process), do: process
  end

  defp io() do
    {{:input, input}, {:output, output}} = :erlang.statistics(:io)
    {input, output}
  end

  defp memory() do
    memory = :erlang.memory()
    total = memory[:total]
    process = memory[:processes]
    atom = memory[:atom]
    binary = memory[:binary]
    code = memory[:code]
    ets = memory[:ets]

    %{
      total: total,
      process: process,
      atom: atom,
      binary: binary,
      code: code,
      ets: ets,
      other: total - process - atom - binary - code - ets
    }
  end

  def memory_allocators_callback(old_max_carrier_sizes) do
    allocs = :erlang.system_info(:alloc_util_allocators)

    :erlang.system_info({:allocator_sizes, allocs})
    |> Enum.map(fn {type, allocator_sizes} ->
      {type, calc_allocator_sizes(allocator_sizes)}
    end)
    |> prepend_total()
    |> Enum.map(fn {type, {block, current_cs, max_cs}} ->
      %{
        name: type,
        block_size: block,
        carrier_size: current_cs,
        max_carrier_size: max_cs
      }
    end)
    |> calc_max_carrier_sizes(old_max_carrier_sizes)
  end

  defp calc_allocator_sizes(allocator_sizes) do
    Enum.reduce(allocator_sizes, {0, 0, 0}, fn instance_sizes, {block_size, current_cs, max_cs} ->
      {ins_block_size, ins_current_cs, ins_max_cs} = calc_instance_sizes(instance_sizes)
      {block_size + ins_block_size, current_cs + ins_current_cs, max_cs + ins_max_cs}
    end)
  end

  defp calc_instance_sizes({:instance, _, sizes}) do
    {block_size_1, current_cs_1, max_cs_1} = calc_block_and_carrier_sizes(sizes[:mbcs])
    {block_size_2, current_cs_2, max_cs_2} = calc_block_and_carrier_sizes(sizes[:sbcs])
    {block_size_1 + block_size_2, current_cs_1 + current_cs_2, max_cs_1 + max_cs_2}
  end

  defp calc_instance_sizes(_), do: {0, 0, 0}

  defp calc_block_and_carrier_sizes(sizes) when is_list(sizes) do
    block_size = List.keyfind(sizes, :blocks, 0) |> calc_block_size()
    {current, max} = List.keyfind(sizes, :carriers_size, 0) |> calc_carrier_size()
    {block_size, current, max}
  end

  defp calc_block_and_carrier_sizes(_), do: {0, 0, 0}

  defp calc_block_size({:blocks, [{_, [{:size, current, _, _}]}]}), do: current
  defp calc_block_size({:blocks, [{_, [{:size, current}]}]}), do: current
  defp calc_block_size(_), do: 0

  defp calc_carrier_size({:carriers_size, current, _local, max}), do: {current, max}
  defp calc_carrier_size({:carriers_size, int}) when is_integer(int), do: {int, 0}
  defp calc_carrier_size(_), do: {0, 0}

  defp prepend_total(allocator_sizes) do
    total =
      {:total,
       Enum.reduce(allocator_sizes, {0, 0, 0}, fn {_type, sizes}, acc ->
         {block_cs, current_cs, max_cs} = sizes
         {acc_block_cs, acc_current_cs, acc_max_cs} = acc
         {acc_block_cs + block_cs, acc_current_cs + current_cs, acc_max_cs + max_cs}
       end)}

    [total | allocator_sizes]
  end

  defp calc_max_carrier_sizes(allocators, old_max_carrier_sizes) do
    Enum.map_reduce(allocators, old_max_carrier_sizes || %{}, fn allocator, max_carrier_sizes ->
      %{name: name, carrier_size: current} = allocator
      max = Enum.max([old_max_carrier_sizes[name] || 0, current])
      {Map.put(allocator, :max_carrier_size, max), Map.put(max_carrier_sizes, name, max)}
    end)
  end

  ## Process Callbacks

  @processes_keys [
    :memory,
    :reductions,
    :message_queue_len,
    :current_function
  ]

  @doc false
  def processes_callback(search, sort_by, sort_dir, limit, prev_reductions) do
    multiplier = sort_dir_multiplier(sort_dir)

    processes =
      for pid <- Process.list(),
          info = process_info(pid, prev_reductions[pid]),
          show_process?(info, search) do
        sorter = info[sort_by] * multiplier
        {sorter, info}
      end

    next_state = for {_sorter, info} <- processes, into: %{}, do: {info[:pid], info[:reductions]}

    count = if search, do: length(processes), else: :erlang.system_info(:process_count)
    processes = processes |> Enum.sort() |> Enum.take(limit) |> Enum.map(&elem(&1, 1))

    {processes, count, next_state}
  end

  defp process_info(pid, prev_reductions) do
    if info = Process.info(pid, @processes_keys) do
      diff = info[:reductions] - (prev_reductions || 0)

      details = to_process_details(pid)

      [
        pid: pid,
        name_or_initial_call: details.name_or_initial_call,
        reductions_diff: diff
      ] ++ info
    end
  end

  defp show_process?(_, nil) do
    true
  end

  defp show_process?(info, search) do
    pid = info[:pid] |> :erlang.pid_to_list() |> List.to_string()
    name_or_call = info[:name_or_initial_call] || ""
    pid =~ search or String.downcase(name_or_call) =~ search
  end

  @process_info_keys [
    :dictionary,
    :links,
    :monitors,
    :monitored_by,
    :registered_name,
    :current_function,
    :status,
    :message_queue_len,
    :trap_exit,
    :error_handler,
    :priority,
    :group_leader,
    :total_heap_size,
    :heap_size,
    :stack_size,
    :reductions,
    :garbage_collection,
    :suspending,
    :current_stacktrace
  ]

  def process_info_callback(pid) do
    case Process.info(pid, @process_info_keys) do
      nil ->
        :error

      info ->
        details = to_process_details(pid)

        {:ok,
         info
         |> Enum.flat_map(&process_info_callback_key/1)
         |> Keyword.put(:initial_call, details.initial_call)}
    end
  end

  defp process_info_callback_key({:links, links}),
    do: [{:links, Enum.map(links, &pid_or_port_details/1)}]

  defp process_info_callback_key({:monitors, monitors}) do
    [
      {:monitors,
       monitors
       |> Enum.map(fn {_label, pid_or_port} -> pid_or_port end)
       |> Enum.map(&pid_or_port_details/1)}
    ]
  end

  defp process_info_callback_key({:monitored_by, monitored_by}),
    do: [{:monitored_by, Enum.map(monitored_by, &pid_or_port_details/1)}]

  defp process_info_callback_key({:group_leader, group_leader}),
    do: [{:group_leader, pid_or_port_details(group_leader)}]

  defp process_info_callback_key({:dictionary, dictionary}) do
    [
      {:ancestors,
       Keyword.get(dictionary, :"$ancestors", [])
       |> Enum.map(&pid_or_port_details/1)},
      {:label, Keyword.get(dictionary, :"$process_label")}
    ]
  end

  defp process_info_callback_key({key, value}), do: [{key, value}]

  ## Applications callbacks

  def applications_info_callback(search, sort_by, sort_dir, limit) do
    sorter = if sort_dir == :asc, do: &<=/2, else: &>=/2
    started_apps_set = started_apps_set()

    apps =
      for {name, desc, version} <- Application.loaded_applications(),
          description = List.to_string(desc),
          version = List.to_string(version),
          show_application?(name, description, version, search) do
        {state, tree?} =
          if name in started_apps_set,
            do: {:started, is_pid(:application_controller.get_master(name))},
            else: {:loaded, false}

        [name: name, description: description, version: version, state: state, tree?: tree?]
      end

    count = length(apps)
    apps = apps |> Enum.sort_by(&Keyword.fetch!(&1, sort_by), sorter) |> Enum.take(limit)
    {apps, count}
  end

  defp show_application?(_, _, _, nil) do
    true
  end

  defp show_application?(name, desc, version, search) do
    Atom.to_string(name) =~ search or String.downcase(desc) =~ search or version =~ search
  end

  defp started_apps_set() do
    Application.started_applications()
    |> Enum.map(fn {name, _, _} -> name end)
    |> MapSet.new()
  end

  def app_tree_callback(app) do
    case :application_controller.get_master(app) do
      :undefined ->
        :error

      master ->
        {child, _app} = :application_master.get_child(master)
        {children, seen} = sup_tree(child, %{master => true, child => true})
        {children, _seen} = links_tree(children, master, seen)

        case get_ancestor(child) do
          nil ->
            {{:master, master, []}, to_wrapped_node(:supervisor, child, children)}

          ancestor ->
            {{:master, master, []},
             [{{:ancestor, ancestor, []}, to_wrapped_node(:supervisor, child, children)}]}
        end
    end
  end

  defp get_ancestor(master) do
    {_, dictionary} = :erlang.process_info(master, :dictionary)

    case Keyword.get(dictionary, :"$ancestors") do
      [parent] -> parent
      _ -> nil
    end
  end

  defp sup_tree(pid, seen) do
    try do
      :supervisor.which_children(pid)
    catch
      _, _ -> {[], seen}
    else
      children ->
        children
        |> Enum.reverse()
        |> Enum.flat_map_reduce(seen, fn {_id, child, type, _modules}, seen ->
          if is_pid(child) do
            {children, seen} = if type == :worker, do: {[], seen}, else: sup_tree(child, seen)
            {[{type, child, children}], put_child(seen, child)}
          else
            {[], seen}
          end
        end)
    end
  end

  defp links_tree(nodes, master, seen) do
    Enum.flat_map_reduce(nodes, seen, fn {type, pid, children}, seen ->
      {children, seen} =
        if children == [], do: links_children(type, pid, master, seen), else: {children, seen}

      {children, seen} = links_tree(children, master, seen)
      {to_wrapped_node(type, pid, children), seen}
    end)
  end

  defp links_children(parent_type, pid, master, seen) do
    # If the parent type is a supervisor and we have no children,
    # then this may be a supervisor bridge, so we tag its children
    # as workers, otherwise they are links.
    type = if parent_type == :supervisor, do: :worker, else: :link

    case Process.info(pid, :links) do
      {:links, children} ->
        children
        |> Enum.reverse()
        |> Enum.flat_map_reduce(seen, fn child, seen ->
          if is_pid(child) and not has_child?(seen, child) and has_leader?(child, master) do
            {[{type, child, []}], put_child(seen, child)}
          else
            {[], seen}
          end
        end)

      _ ->
        {[], seen}
    end
  end

  defp to_wrapped_node(type, pid, children) do
    case Process.info(pid, [:registered_name, :dictionary]) do
      [{:registered_name, []}, {:dictionary, dictionary}] ->
        label = Keyword.get(dictionary, :"$process_label", [])
        [{{type, pid, label}, children}]

      [{:registered_name, registered_name}, _] ->
        [{{type, pid, registered_name}, children}]

      _ ->
        []
    end
  end

  defp has_child?(seen, child), do: Map.has_key?(seen, child)
  defp put_child(seen, child), do: Map.put(seen, child, true)

  defp has_leader?(pid, gl),
    do: Process.info(pid, :group_leader) == {:group_leader, gl}

  ## Ports callbacks

  @inet_ports [~c"tcp_inet", ~c"udp_inet", ~c"sctp_inet"]

  @doc false
  def ports_callback(search, sort_by, sort_dir, limit) do
    multiplier = sort_dir_multiplier(sort_dir)

    ports =
      for port <- Port.list(), port_info = port_info(port), show_port?(port_info, search) do
        sorter = port_info[sort_by]
        sorter = if is_integer(sorter), do: sorter * multiplier, else: 0
        {sorter, port_info}
      end

    count = length(ports)
    ports = ports |> Enum.sort() |> Enum.take(limit) |> Enum.map(&elem(&1, 1))
    {ports, count}
  end

  @doc false
  def port_info_callback(port) do
    case Port.info(port) do
      [_ | _] = info ->
        {:ok,
         info
         |> Keyword.update!(:links, fn links -> Enum.map(links, &pid_or_port_details/1) end)
         |> Keyword.update!(:connected, &pid_or_port_details/1)}

      nil ->
        :error
    end
  end

  defp port_info(port) do
    info = Port.info(port)

    if info && info[:name] not in @inet_ports do
      [port: port] ++ info
    end
  end

  defp show_port?(_, nil) do
    true
  end

  defp show_port?(info, search) do
    port = info[:port] |> :erlang.port_to_list() |> List.to_string()
    port =~ search or String.downcase(List.to_string(info[:name])) =~ search
  end

  ## ETS callbacks

  def ets_callback(search, sort_by, sort_dir, limit) do
    multiplier = sort_dir_multiplier(sort_dir)

    tables =
      for ref <- :ets.all(), info = ets_info(ref), show_ets?(info, search) do
        sorter = info[sort_by] * multiplier
        {sorter, info}
      end

    count = length(tables)
    tables = tables |> Enum.sort() |> Enum.take(limit) |> Enum.map(&elem(&1, 1))
    {tables, count}
  end

  defp ets_info(ref) do
    case :ets.info(ref) do
      :undefined -> nil
      info -> [name: inspect(info[:name])] ++ Keyword.delete(info, :name)
    end
  end

  defp show_ets?(_, nil) do
    true
  end

  defp show_ets?(info, search) do
    String.downcase(info[:name]) =~ search
  end

  def ets_info_callback(ref) do
    case :ets.info(ref) do
      :undefined ->
        :error

      info ->
        {:ok,
         info
         |> Keyword.update!(:owner, &pid_or_port_details/1)
         |> Keyword.update!(:heir, fn
           :none -> nil
           heir -> pid_or_port_details(heir)
         end)}
    end
  end

  ## Socket callbacks

  def sockets_callback(search, sort_by, sort_dir, limit) do
    sorter = if sort_dir == :asc, do: &<=/2, else: &>=/2
    sockets = Port.list() ++ gen_tcp_sockets() ++ gen_udp_sockets()

    sockets = for port <- sockets, info = socket_info(port), show_socket?(info, search), do: info

    count = length(sockets)
    sockets = sockets |> Enum.sort_by(&Keyword.fetch!(&1, sort_by), sorter) |> Enum.take(limit)
    {sockets, count}
  end

  def socket_info_callback(port) do
    case socket_info(port) do
      nil ->
        :error

      info ->
        {:ok,
         info
         |> Keyword.update!(:connected, &pid_or_port_details/1)}
    end
  end

  defp gen_tcp_sockets() do
    if function_exported?(:gen_tcp_socket, :which_sockets, 0) do
      apply(:gen_tcp_socket, :which_sockets, [])
    else
      []
    end
  end

  defp gen_udp_sockets() do
    if function_exported?(:gen_udp_socket, :which_sockets, 0) do
      apply(:gen_udp_socket, :which_sockets, [])
    else
      []
    end
  end

  defp socket_info({:"$inet", gen_socket_mod, {pid, {:"$socket", _ref}}} = socket) do
    with info when not is_nil(info) <- gen_socket_mod.info(socket),
         port <- get_socket_fd(socket, gen_socket_mod) do
      [
        module: gen_socket_mod,
        port: port,
        local_address: format_address(gen_socket_mod.sockname(socket)),
        foreign_address: format_address(gen_socket_mod.peername(socket)),
        state: format_socket_state(info[:rstates]),
        type: info[:type],
        connected: pid,
        send_oct: info[:counters][:send_oct],
        recv_oct: info[:counters][:recv_oct]
      ]
    else
      _ -> nil
    end
  end

  defp socket_info(port) do
    with info when not is_nil(info) <- Port.info(port),
         true <- info[:name] in @inet_ports,
         {:ok, stat} <- :inet.getstat(port, [:send_oct, :recv_oct]),
         {:ok, state} <- :prim_inet.getstatus(port),
         {:ok, {_, type}} <- :prim_inet.gettype(port),
         module <- inet_module_lookup(port) do
      [
        module: module,
        port: port,
        local_address: format_address(:inet.sockname(port)),
        foreign_address: format_address(:inet.peername(port)),
        state: format_socket_state(state),
        type: type,
        connected: info[:connected]
      ] ++ stat
    else
      _ -> nil
    end
  end

  defp get_socket_fd(socket, gen_socket_mod) do
    case gen_socket_mod.getopts(socket, [:fd]) do
      {:ok, [fd: fd]} -> "esock[#{fd}]"
      _ -> "esock"
    end
  end

  defp show_socket?(_info, nil), do: true

  defp show_socket?(info, search) do
    info[:local_address] =~ search || info[:foreign_address] =~ search
  end

  defp inet_module_lookup(port) do
    case :inet_db.lookup_socket(port) do
      {:ok, module} -> module
      _ -> "prim_inet"
    end
  end

  ### OS_Mon callbacks

  def os_mon_callback() do
    cpu_per_core =
      case :cpu_sup.util([:detailed, :per_cpu]) do
        {:all, 0, 0, []} -> []
        cores -> Enum.map(cores, fn {n, busy, non_b, _} -> {n, Map.new(busy ++ non_b)} end)
      end

    disk =
      case :disksup.get_disk_data() do
        [{~c"none", 0, 0}] -> []
        other -> other
      end

    %{
      cpu_avg1: :cpu_sup.avg1(),
      cpu_avg5: :cpu_sup.avg5(),
      cpu_avg15: :cpu_sup.avg15(),
      cpu_nprocs: :cpu_sup.nprocs(),
      cpu_per_core: cpu_per_core,
      disk: disk,
      system_mem: :memsup.get_system_memory_data()
    }
  end

  ### Environment info callbacks

  def env_info_callback(nil), do: nil

  def env_info_callback(keys) do
    Enum.map(keys, fn key -> {key, System.get_env(key)} end)
  end

  ## Helpers

  # The address is formatted based on the implementation of `:inet.fmt_addr/2`
  defp format_address({:error, :enotconn}), do: "*:*"
  defp format_address({:error, _}), do: " "

  defp format_address({:ok, address}) do
    case address do
      {{0, 0, 0, 0}, port} -> "*:#{port}"
      {{0, 0, 0, 0, 0, 0, 0, 0}, port} -> "*:#{port}"
      {{127, 0, 0, 1}, port} -> "localhost:#{port}"
      {{0, 0, 0, 0, 0, 0, 0, 1}, port} -> "localhost:#{port}"
      {:local, path} -> "local:#{path}"
      {ip, port} -> "#{:inet.ntoa(ip)}:#{port}"
    end
  end

  # See `:inet.fmt_status`
  defp format_socket_state(flags) do
    case Enum.sort(flags) do
      [:accepting | _] -> "ACCEPTING"
      [:bound, :busy, :connected | _] -> "BUSY"
      [:bound, :connected | _] -> "CONNECTED"
      [:bound, :listen, :listening | _] -> "LISTENING"
      [:bound, :listen | _] -> "LISTEN"
      [:bound, :connecting | _] -> "CONNECTING"
      [:bound, :open] -> "BOUND"
      [:bound, :selected] -> "CONNECTED"
      [:connected, :open] -> "CONNECTED"
      [:open] -> "IDLE"
      [] -> "CLOSED"
      sorted -> inspect(sorted)
    end
  end

  defp sort_dir_multiplier(:asc), do: 1
  defp sort_dir_multiplier(:desc), do: -1

  defp pid_or_port_details(pid) when is_pid(pid), do: to_process_details(pid)
  defp pid_or_port_details(name) when is_atom(name), do: to_process_details(name)
  defp pid_or_port_details(port) when is_port(port), do: to_port_details(port)
  defp pid_or_port_details(reference) when is_reference(reference), do: reference

  def to_process_details(pid) when is_pid(pid) and node(pid) == node() do
    {name, initial_call} =
      case Process.info(pid, [:initial_call, :dictionary, :registered_name]) do
        [{:initial_call, initial_call}, {:dictionary, dictionary}, {:registered_name, name}] ->
          initial_call = Keyword.get(dictionary, :"$initial_call", initial_call)

          name =
            format_registered_name(name) ||
              format_process_label(Keyword.get(dictionary, :"$process_label")) ||
              format_initial_call(initial_call)

          {name, initial_call}

        _ ->
          {nil, nil}
      end

    %ProcessDetails{pid: pid, name_or_initial_call: name, initial_call: initial_call}
  end

  def to_process_details(pid) when is_pid(pid) do
    %ProcessDetails{pid: pid, name_or_initial_call: nil, initial_call: nil}
  end

  def to_process_details(name) when is_atom(name) do
    Process.whereis(name)
    |> to_process_details()
  end

  defp format_process_label(nil), do: nil
  defp format_process_label(label) when is_binary(label), do: label
  defp format_process_label(label), do: inspect(label)

  defp format_registered_name([]), do: nil
  defp format_registered_name(name), do: inspect(name)

  defp format_initial_call({:supervisor, mod, arity}), do: Exception.format_mfa(mod, :init, arity)
  defp format_initial_call({m, f, a}), do: Exception.format_mfa(m, f, a)
  defp format_initial_call(nil), do: nil

  def to_port_details(port) when is_port(port) do
    description =
      case Port.info(port, :name) do
        {:name, name} -> name
        _ -> port
      end

    %PortDetails{port: port, description: description}
  end
end
