Remove unused unit parameter

Remove unused unit parameter

For this version of the function, the label and separator are
explicit, so there is no need for the unit

defmodule Benchee.Conversion.Format do
  @moduledoc """
  Functions for formatting values and their unit labels. Different domains
  handle this task differently, for example durations and counts.

  See `Benchee.Conversion.Count` and `Benchee.Conversion.Duration` for examples
  """

  alias Benchee.Conversion.Unit

  @doc """
  Formats a number as a string, with a unit label. See `Benchee.Conversion.Count`
  and `Benchee.Conversion.Duration` for examples
  """
  @callback format(number) :: String.t

  # Generic formatting functions

  @doc """
  Formats a unit value with specified label and separator
  """
  def format(count, label, separator) do
    separator = separator(label, separator)
    "~.#{float_precision(count)}f~ts~ts"
    |> :io_lib.format([count, separator, label])
    |> to_string
  end

  @doc """
  Formats a unit value in the domain described by `module`. The module should
  provide a `units/0` function that returns a Map like

      %{ :unit_name => %Benchee.Conversion.Unit{ ... } }

  Additionally, `module` may specify a `separator/0` function, which provides a
  custom separator string that will appear between the value and label in the
  formatted output. If no `separator/0` function exists, the default separator
  (a single space) will be used.
  """
  def format({count, unit}, module) do
    format(count, label(module, unit), separator(module))
  end

  @doc """
  Scales a number to the most appropriate unit as defined in `module`, and
  formats the scaled value with a label. The module should provide a `units/0`
  function that returns a Map like

      %{ :unit_name => %Benchee.Conversion.Unit{ ... } }
  """
  def format(number, module) do
    number
    |> module.scale
    |> format(module)
  end

  # Returns the separator defined in `module.separator/0`, or the default, a space
  defp separator(module) do
    case function_exported?(module, :separator, 0) do
      true  -> module.separator()
      false -> " "
    end
  end

  # Returns the separator, or an empty string if there isn't a label
  defp separator(label, _separator) when label == "" or label == nil, do: ""
  defp separator(_label, separator), do: separator

  # Fetches the label for the given unit
  defp label(module, unit) do
    Unit.label(module, unit)
  end

  defp float_precision(float) when float < 0.01, do: 5
  defp float_precision(float) when float < 0.1, do: 4
  defp float_precision(float) when float < 0.2, do: 3
  defp float_precision(_float), do: 2
end

defmodule Benchee.Conversion.Format do
  @moduledoc """
  Functions for formatting values and their unit labels. Different domains
  handle this task differently, for example durations and counts.

  See `Benchee.Conversion.Count` and `Benchee.Conversion.Duration` for examples
  """

  alias Benchee.Conversion.Unit

  @doc """
  Formats a number as a string, with a unit label. See `Benchee.Conversion.Count`
  and `Benchee.Conversion.Duration` for examples
  """
  @callback format(number) :: String.t

  # Generic formatting functions

  @doc """
  Formats a unit value with specified label and separator
  """
  def format({count, _unit}, label, separator) do
    separator = separator(label, separator)
    "~.#{float_precision(count)}f~ts~ts"
    |> :io_lib.format([count, separator, label])
    |> to_string
  end

  @doc """
  Formats a unit value in the domain described by `module`. The module should
  provide a `units/0` function that returns a Map like

      %{ :unit_name => %Benchee.Conversion.Unit{ ... } }

  Additionally, `module` may specify a `separator/0` function, which provides a
  custom separator string that will appear between the value and label in the
  formatted output. If no `separator/0` function exists, the default separator
  (a single space) will be used.
  """
  def format({count, unit}, module) do
    format({count, unit}, label(module, unit), separator(module))
  end

  @doc """
  Scales a number to the most appropriate unit as defined in `module`, and
  formats the scaled value with a label. The module should provide a `units/0`
  function that returns a Map like

      %{ :unit_name => %Benchee.Conversion.Unit{ ... } }
  """
  def format(number, module) do
    number
    |> module.scale
    |> format(module)
  end

  # Returns the separator defined in `module.separator/0`, or the default, a space
  defp separator(module) do
    case function_exported?(module, :separator, 0) do
      true  -> module.separator()
      false -> " "
    end
  end

  # Returns the separator, or an empty string if there isn't a label
  defp separator(label, _separator) when label == "" or label == nil, do: ""
  defp separator(_label, separator), do: separator

  # Fetches the label for the given unit
  defp label(module, unit) do
    Unit.label(module, unit)
  end

  defp float_precision(float) when float < 0.01, do: 5
  defp float_precision(float) when float < 0.1, do: 4
  defp float_precision(float) when float < 0.2, do: 3
  defp float_precision(_float), do: 2
end


Fix ambiguous pipe

Fix ambiguous pipe

defmodule Farmbot.Firmware do
  @moduledoc "Allows communication with the firmware."

  use GenStage
  use Farmbot.Logger
  alias Farmbot.Bootstrap.SettingsSync
  alias Farmbot.Firmware.{Command, CompletionLogs, Vec3, EstopTimer, Utils}
  import Utils

  import Farmbot.System.ConfigStorage,
    only: [get_config_value: 3, update_config_value: 4, get_config_as_map: 0]

  import CompletionLogs,
    only: [maybe_log_complete: 2]

  # If any command takes longer than this, exit.
  @call_timeout 500_000

  @doc "Move the bot to a position."
  def move_absolute(%Vec3{} = vec3, x_spd, y_spd, z_spd) do
    call = {:move_absolute, [vec3, x_spd, y_spd, z_spd]}
    GenStage.call(__MODULE__, call, @call_timeout)
  end

  @doc "Calibrate an axis."
  def calibrate(axis) do
    GenStage.call(__MODULE__, {:calibrate, [axis]}, @call_timeout)
  end

  @doc "Find home on an axis."
  def find_home(axis) do
    GenStage.call(__MODULE__, {:find_home, [axis]}, @call_timeout)
  end

  @doc "Home every axis."
  def home_all() do
    GenStage.call(__MODULE__, {:home_all, []}, @call_timeout)
  end

  @doc "Home an axis."
  def home(axis) do
    GenStage.call(__MODULE__, {:home, [axis]}, @call_timeout)
  end

  @doc "Manually set an axis's current position to zero."
  def zero(axis) do
    GenStage.call(__MODULE__, {:zero, [axis]}, @call_timeout)
  end

  @doc """
  Update a paramater.
  For a list of paramaters see `Farmbot.Firmware.Gcode.Param`
  """
  def update_param(param, val) do
    GenStage.call(__MODULE__, {:update_param, [param, val]}, @call_timeout)
  end

  @doc false
  def read_all_params do
    GenStage.call(__MODULE__, {:read_all_params, []}, @call_timeout)
  end

  @doc """
  Read a paramater.
  For a list of paramaters see `Farmbot.Firmware.Gcode.Param`
  """
  def read_param(param) do
    GenStage.call(__MODULE__, {:read_param, [param]}, @call_timeout)
  end

  @doc "Emergency lock Farmbot."
  def emergency_lock() do
    GenStage.call(__MODULE__, {:emergency_lock, []}, @call_timeout)
  end

  @doc "Unlock Farmbot from Emergency state."
  def emergency_unlock() do
    GenStage.call(__MODULE__, {:emergency_unlock, []}, @call_timeout)
  end

  @doc "Set a pin mode (`:input` | `:output` | `:input_pullup`)"
  def set_pin_mode(pin, mode) do
    GenStage.call(__MODULE__, {:set_pin_mode, [pin, mode]}, @call_timeout)
  end

  @doc "Read a pin."
  def read_pin(pin, mode) do
    GenStage.call(__MODULE__, {:read_pin, [pin, mode]}, @call_timeout)
  end

  @doc "Write a pin."
  def write_pin(pin, mode, value) do
    GenStage.call(__MODULE__, {:write_pin, [pin, mode, value]}, @call_timeout)
  end

  @doc "Request version."
  def request_software_version do
    GenStage.call(__MODULE__, {:request_software_version, []}, @call_timeout)
  end

  @doc "Set angle of a servo pin."
  def set_servo_angle(pin, value) do
    GenStage.call(__MODULE__, {:set_servo_angle, [pin, value]}, @call_timeout)
  end

  @doc "Flag for all params reported."
  def params_reported do
    GenStage.call(__MODULE__, :params_reported)
  end

  @doc "Start the firmware services."
  def start_link do
    GenStage.start_link(__MODULE__, [], name: __MODULE__)
  end

  ## GenStage

  defmodule State do
    @moduledoc false
    defstruct [
      handler: nil,
      handler_mod: nil,
      idle: false,
      timer: nil,
      pins: %{},
      params: %{},
      params_reported: false,
      initialized: false,
      initializing: false,
      current: nil,
      timeout_ms: 150_000,
      queue: :queue.new(),
      x_needs_home_on_boot: false,
      y_needs_home_on_boot: false,
      z_needs_home_on_boot: false
    ]
  end

  defp needs_home_on_boot do
    x = (get_config_value(:float, "hardware_params", "movement_home_at_boot_x") || 0)
    |> num_to_bool()

    y = (get_config_value(:float, "hardware_params", "movement_home_at_boot_y") || 0)
    |> num_to_bool()

    z = (get_config_value(:float, "hardware_params", "movement_home_at_boot_z") || 0)
    |> num_to_bool()

    %{
      x_needs_home_on_boot: x,
      y_needs_home_on_boot: y,
      z_needs_home_on_boot: z,
    }
  end

  def init([]) do
    handler_mod =
      Application.get_env(:farmbot, :behaviour)[:firmware_handler] || raise("No fw handler.")

    case handler_mod.start_link() do
      {:ok, handler} ->
        initial = Map.merge(needs_home_on_boot(), %{handler: handler, handler_mod: handler_mod})
        Process.flag(:trap_exit, true)
        {
          :producer_consumer,
          struct(State, initial),
          subscribe_to: [handler], dispatcher: GenStage.BroadcastDispatcher
        }
      {:error, reason} ->
        replace_firmware_handler(Farmbot.Firmware.StubHandler)
        Logger.error 1, "Failed to initialize firmware: #{inspect reason} Falling back to stub implementation."
        init([])
    end

  end

  def terminate(reason, state) do
    unless reason in [:normal, :shutdown] do
      replace_firmware_handler(Farmbot.Firmware.StubHandler)
    end

    unless :queue.is_empty(state.queue) do
      list = :queue.to_list(state.queue)
      for cmd <- list do
        :ok = do_reply(%{state | current: cmd}, {:error, reason})
      end
    end
  end

  def handle_info({:EXIT, _pid, :normal}, state) do
    {:stop, :normal, state}
  end

  def handle_info({:EXIT, _, reason}, state) do
    Logger.error 1, "Firmware handler: #{state.handler_mod} died: #{inspect reason}"
    case state.handler_mod.start_link() do
      {:ok, handler} ->
        new_state = %{state | handler: handler}
        {:noreply, [{:informational_settings, %{busy: false}}], %{new_state | initialized: false, idle: false}}
      err -> {:stop, err, %{state | handler: false}}
    end
  end

  # TODO(Connor): Put some sort of exit strategy here.
  # If a firmware command keeps timingout/failing, Farmbot OS just keeps trying
  # it. This can lead to infinate failures.
  def handle_info({:command_timeout, %Command{} = timeout_command}, state) do
    case state.current do
      # Check if this timeout is actually talking about the current command.
      ^timeout_command = current ->
        Logger.warn 1, "Timed out waiting for Firmware response. Retrying #{inspect current}) "
        case apply(state.handler_mod, current.fun, [state.handler | current.args]) do
          :ok ->
            timer = start_timer(current, state.timeout_ms)
            {:noreply, [], %{state | current: current, timer: timer}}
          {:error, _} = res ->
            do_reply(state, res)
            {:noreply, [], %{state | current: nil, queue: :queue.new()}}
        end

      # If this timeout was not talking about the current command
      %Command{} = current ->
        Logger.debug 3, "Got stray timeout for command: #{inspect current}"
        {:noreply, [], %{state | timer: nil}}

      # If there is no current command, we got a different kind of stray.
      # This is ok i guess.
      nil -> {:noreply, [], %{state | timer: nil}}

    end
  end

  def handle_call(:params_reported, _, state) do
    {:reply, state.params_reported, [], state}
  end

  def handle_call({fun, _}, _from, state = %{initialized: false})
  when fun not in  [:read_all_params, :update_param, :emergency_unlock, :emergency_lock, :request_software_version] do
    {:reply, {:error, :uninitialized}, [], state}
  end

  def handle_call({fun, args}, from, state) do
    next_current = struct(Command, from: from, fun: fun, args: args)
    current_current = state.current
    cond do
      fun == :emergency_lock ->
        if current_current do
          do_reply(state, {:error, :emergency_lock})
        end
        do_begin_cmd(next_current, state, [])
      match?(%Command{}, current_current) ->
        do_queue_cmd(next_current, state)
      is_nil(current_current) ->
        do_begin_cmd(next_current, state, [])
    end
  end

  defp do_begin_cmd(%Command{fun: fun, args: args, from: _from} = current, state, dispatch) do
    case apply(state.handler_mod, fun, [state.handler | args]) do
      :ok ->
        timer = start_timer(current, state.timeout_ms)
        if fun == :emergency_unlock do
          Farmbot.System.GPIO.Leds.led_status_ok()
          new_dispatch = [{:informational_settings,  %{busy: false, locked: false}} | dispatch]
          {:noreply, new_dispatch, %{state | current: current, timer: timer}}
        else
          {:noreply, dispatch, %{state | current: current, timer: timer}}
        end
      {:error, _} = res ->
        do_reply(%{state | current: current}, res)
        {:noreply, dispatch, %{state | current: nil}}
    end
  end

  defp do_queue_cmd(%Command{fun: _fun, args: _args, from: _from} = current, state) do
    # Logger.busy 3, "FW Queuing: #{fun}: #{inspect from}"
    new_q = :queue.in(current, state.queue)
    {:noreply, [], %{state | queue: new_q}}
  end

  def handle_events(gcodes, _from, state) do
    {diffs, state} = handle_gcodes(gcodes, state)
    # if after handling the current buffer of gcodes,
    # Try to start the next command in the queue if it exists.
    if List.last(gcodes) == :idle && state.current == nil do
      case :queue.out(state.queue) do
        {{:value, next_current}, new_queue} ->
          do_begin_cmd(next_current, %{state | queue: new_queue, current: next_current}, diffs)
        {:empty, queue} -> # nothing to do if the queue is empty.
          {:noreply, diffs, %{state | queue: queue}}
      end
    else
      {:noreply, diffs, state}
    end
  end

  defp handle_gcodes(codes, state, acc \\ [])

  defp handle_gcodes([], state, acc), do: {Enum.reverse(acc), state}

  defp handle_gcodes([code | rest], state, acc) do
    case handle_gcode(code, state) do
      {nil, new_state} -> handle_gcodes(rest, new_state, acc)
      {key, diff, new_state} -> handle_gcodes(rest, new_state, [{key, diff} | acc])
    end
  end

  defp handle_gcode({:debug_message, message}, state) do
    if get_config_value(:bool, "settings", "arduino_debug_messages") do
      Logger.debug 3, "Arduino debug message: #{message}"
    end
    {nil, state}
  end

  defp handle_gcode(code, state) when code in [:error, :invalid_command] do
    maybe_cancel_timer(state.timer, state.current)
    if state.current do
      Logger.error 1, "Got #{code} while executing `#{inspect state.current}`."
      do_reply(state, {:error, :firmware_error})
      {nil, %{state | current: nil}}
    else
      {nil, state}
    end
  end

  defp handle_gcode(:report_no_config, state) do
    Logger.busy 1, "Initializing Firmware."
    old = get_config_as_map()["hardware_params"]
    spawn __MODULE__, :do_read_params, [Map.delete(old, "param_version")]
    {nil, %{state | initialized: false, initializing: true}}
  end

  defp handle_gcode(:report_params_complete, state) do
    {nil, %{state | initialized: true, initializing: false}}
  end

  defp handle_gcode(:idle, %{initialized: false, initializing: false} = state) do
    Logger.busy 1, "Firmware not initialized yet. Waiting for R88 message."
    {nil, state}
  end

  defp handle_gcode(:idle, %{initialized: true, initializing: false, current: nil, z_needs_home_on_boot: true} = state) do
    Logger.info 2, "Bootup homing Z axis"
    spawn __MODULE__, :find_home, [:z]
    {nil, %{state | z_needs_home_on_boot: false}}
  end

  defp handle_gcode(:idle, %{initialized: true, initializing: false, current: nil, y_needs_home_on_boot: true} = state) do
    Logger.info 2, "Bootup homing Y axis"
    spawn __MODULE__, :find_home, [:y]
    {nil, %{state | y_needs_home_on_boot: false}}
  end

  defp handle_gcode(:idle, %{initialized: true, initializing: false, current: nil, x_needs_home_on_boot: true} = state) do
    Logger.info 2, "Bootup homing X axis"
    spawn __MODULE__, :find_home, [:x]
    {nil, %{state | x_needs_home_on_boot: false}}
  end

  defp handle_gcode(:idle, state) do
    maybe_cancel_timer(state.timer, state.current)
    Farmbot.BotState.set_busy(false)
    if state.current do
      # This might be a bug in the FW
      if state.current.fun in [:home, :home_all] do
        Logger.warn 1, "Got idle during home."
        timer = start_timer(state.current, state.timeout_ms)
        {nil, %{state | timer: timer}}
      else
        Logger.warn 1, "Got idle while executing a command."
        do_reply(state, {:error, :timeout})
        {:informational_settings, %{busy: false, locked: false}, %{state | current: nil, idle: true}}
      end
    else
      {:informational_settings, %{busy: false, locked: false}, %{state | idle: true}}
    end
  end

  defp handle_gcode({:report_current_position, x, y, z}, state) do
    {:location_data, %{position: %{x: round(x), y: round(y), z: round(z)}}, state}
  end

  defp handle_gcode({:report_encoder_position_scaled, x, y, z}, state) do
    {:location_data, %{scaled_encoders: %{x: x, y: y, z: z}}, state}
  end

  defp handle_gcode({:report_encoder_position_raw, x, y, z}, state) do
    {:location_data, %{raw_encoders: %{x: x, y: y, z: z}}, state}
  end

  defp handle_gcode({:report_end_stops, xa, xb, ya, yb, za, zb}, state) do
    diff = %{end_stops: %{xa: xa, xb: xb, ya: ya, yb: yb, za: za, zb: zb}}
    {:location_data, diff, state}
    {nil, state}
  end

  defp handle_gcode({:report_pin_mode, pin, mode_atom}, state) do
    # Logger.debug 3, "Got pin mode report: #{pin}: #{mode_atom}"
    mode = extract_pin_mode(mode_atom)
    case state.pins[pin] do
      %{mode: _, value: _} = pin_map ->
        {:pins, %{pin => %{pin_map | mode: mode}}, %{state | pins: %{state.pins | pin => %{pin_map | mode: mode}}}}
      nil ->
        {:pins, %{pin => %{mode: mode, value: -1}}, %{state | pins: Map.put(state.pins, pin, %{mode: mode, value: -1})}}
    end
  end

  defp handle_gcode({:report_pin_value, pin, value}, state) do
    # Logger.debug 3, "Got pin value report: #{pin}: #{value} old: #{inspect state.pins[pin]}"
    case state.pins[pin] do
      %{mode: _, value: _} = pin_map ->
        {:pins, %{pin => %{pin_map | value: value}}, %{state | pins: %{state.pins | pin => %{pin_map | value: value}}}}
      nil ->
        {:pins, %{pin => %{mode: nil, value: value}}, %{state | pins: Map.put(state.pins, pin, %{mode: nil, value: value})}}
    end
  end

  defp handle_gcode({:report_parameter_value, param, value}, state) when (value == -1) do
    maybe_update_param_from_report(to_string(param), nil)
    {:mcu_params, %{param => nil}, %{state | params: Map.put(state.params, param, value)}}
  end

  defp handle_gcode({:report_parameter_value, param, value}, state) when is_number(value) do
    maybe_update_param_from_report(to_string(param), value)
    {:mcu_params, %{param => value}, %{state | params: Map.put(state.params, param, value)}}
  end

  defp handle_gcode({:report_software_version, version}, state) do
    case String.last(version) do
      "F" ->
        update_config_value(:string, "settings", "firmware_hardware", "farmduino")
      "R" ->
        update_config_value(:string, "settings", "firmware_hardware", "arduino")
      "G" ->
        update_config_value(:string, "settings", "firmware_hardware", "farmduino_k14")
      _ -> :ok
    end
    {:informational_settings, %{firmware_version: version}, state}
  end

  defp handle_gcode(:report_axis_home_complete_x, state) do
    {nil, state}
  end

  defp handle_gcode(:report_axis_home_complete_y, state) do
    {nil, state}
  end

  defp handle_gcode(:report_axis_home_complete_z, state) do
    {nil, %{state | timer: nil}}
  end

  defp handle_gcode(:report_axis_timeout_x, state) do
    do_reply(state, {:error, :axis_timeout_x})
    {nil, %{state | timer: nil}}
  end

  defp handle_gcode(:report_axis_timeout_y, state) do
    do_reply(state, {:error, :axis_timeout_y})
    {nil, %{state | timer: nil}}
  end

  defp handle_gcode(:report_axis_timeout_z, state) do
    do_reply(state, {:error, :axis_timeout_z})
    {nil, %{state | timer: nil}}
  end

  defp handle_gcode({:report_axis_changed_x, _new_x} = msg, state) do
    new_current = Command.add_status(state.current, msg)
    {nil, %{state | current: new_current}}
  end

  defp handle_gcode({:report_axis_changed_y, _new_y} = msg, state) do
    new_current = Command.add_status(state.current, msg)
    {nil, %{state | current: new_current}}
  end

  defp handle_gcode({:report_axis_changed_z, _new_z} = msg, state) do
    new_current = Command.add_status(state.current, msg)
    {nil, %{state | current: new_current}}
  end

  defp handle_gcode(:busy, state) do
    Farmbot.BotState.set_busy(true)
    maybe_cancel_timer(state.timer, state.current)
    timer = if state.current do
      start_timer(state.current, state.timeout_ms)
    else
      nil
    end
    {:informational_settings, %{busy: true}, %{state | idle: false, timer: timer}}
  end

  defp handle_gcode(:done, state) do
    maybe_cancel_timer(state.timer, state.current)
    Farmbot.BotState.set_busy(false)
    if state.current do
      do_reply(state, :ok)
      {nil, %{state | current: nil}}
    else
      {nil, state}
    end
  end

  defp handle_gcode(:report_emergency_lock, state) do
    Farmbot.System.GPIO.Leds.led_status_err
    maybe_send_email()
    if state.current do
      do_reply(state, {:error, :emergency_lock})
      {:informational_settings, %{locked: true}, %{state | current: nil}}
    else
      {:informational_settings, %{locked: true}, state}
    end
  end

  defp handle_gcode({:report_calibration, axis, status}, state) do
    maybe_cancel_timer(state.timer, state.current)
    Logger.busy 1, "Axis #{axis} calibration: #{status}"
    {nil, state}
  end

  defp handle_gcode({:report_axis_calibration, param, val}, state) do
    spawn __MODULE__, :report_calibration_callback, [5, param, val]
    {nil, state}
  end

  defp handle_gcode(:noop, state) do
    {nil, state}
  end

  defp handle_gcode(:received, state) do
    {nil, state}
  end

  defp handle_gcode({:echo, _code}, state) do
    {nil, state}
  end

  defp handle_gcode(code, state) do
    Logger.warn(3, "unhandled code: #{inspect(code)}")
    {nil, state}
  end

  defp maybe_cancel_timer(nil, current_command) do
    if current_command do
      # Logger.debug 3, "[WEIRD] - No timer to cancel for command: #{inspect current_command}"
      :ok
    else
      # Logger.debug 3, "[PROBABLY OK] - No timer to cancel, and no command here."
      :ok
    end
  end

  defp maybe_cancel_timer(timer, _current_command) do
    if Process.read_timer(timer) do
      # Logger.debug 3, "[NORMAL] - Canceled timer: #{inspect timer} for command: #{inspect current_command}"
      Process.cancel_timer(timer)
      :ok
    else
      :ok
    end
  end

  defp maybe_update_param_from_report(param, val) when is_binary(param) do
    real_val = if val, do: (val / 1), else: nil
    # Logger.debug 3, "Firmware reported #{param} => #{val || -1}"
    update_config_value(:float, "hardware_params", to_string(param), real_val)
  end

  @doc false
  def do_read_params(old) when is_map(old) do
    for {key, float_val} <- old do
      cond do
        (float_val == -1) -> :ok
        is_nil(float_val) -> :ok
        is_number(float_val) ->
          val = round(float_val)
          :ok = update_param(:"#{key}", val)
      end
    end
    :ok = update_param(:param_use_eeprom, 0)
    :ok = update_param(:param_config_ok, 1)
    read_all_params()
    :ok = request_software_version()
  end

  @doc false
  def report_calibration_callback(tries, param, value)

  def report_calibration_callback(0, _param, _value) do
    :ok
  end

  def report_calibration_callback(tries, param, val) do
    case Farmbot.Firmware.update_param(param, val) do
      :ok ->
        str_param = to_string(param)
        case get_config_value(:float, "hardware_params", str_param) do
          ^val ->
            Logger.success 1, "Calibrated #{param}: #{val}"
            SettingsSync.upload_fw_kv(str_param, val)
            :ok
          _ -> report_calibration_callback(tries - 1, param, val)
        end
      {:error, reason} ->
        Logger.error 1, "Failed to set #{param}: #{val} (#{inspect reason})"
        report_calibration_callback(tries - 1, param, val)
    end
  end

  defp do_reply(state, reply) do
    maybe_cancel_timer(state.timer, state.current)
    maybe_log_complete(state.current, reply)
    case state.current do
      %Command{fun: :emergency_unlock, from: from} ->
        # i really don't want this to be here..
        EstopTimer.cancel_timer()
        :ok = GenServer.reply from, reply
      %Command{fun: :emergency_lock, from: from} ->
        :ok = GenServer.reply from, {:error, :emergency_lock}
      %Command{fun: _fun, from: from} ->
        # Logger.success 3, "FW Replying: #{fun}: #{inspect from}"
        :ok = GenServer.reply from, reply
      nil ->
        Logger.error 1, "FW Nothing to send reply: #{inspect reply} to!."
        :error
    end
  end

  defp maybe_send_email do
    if get_config_value(:bool, "settings", "email_on_estop") do
      if !EstopTimer.timer_active? do
        EstopTimer.start_timer()
      end
    end
  end

  defp start_timer(%Command{} = command, timeout) do
    Process.send_after(self(), {:command_timeout, command}, timeout)
  end
end

defmodule Farmbot.Firmware do
  @moduledoc "Allows communication with the firmware."

  use GenStage
  use Farmbot.Logger
  alias Farmbot.Bootstrap.SettingsSync
  alias Farmbot.Firmware.{Command, CompletionLogs, Vec3, EstopTimer, Utils}
  import Utils

  import Farmbot.System.ConfigStorage,
    only: [get_config_value: 3, update_config_value: 4, get_config_as_map: 0]

  import CompletionLogs,
    only: [maybe_log_complete: 2]

  # If any command takes longer than this, exit.
  @call_timeout 500_000

  @doc "Move the bot to a position."
  def move_absolute(%Vec3{} = vec3, x_spd, y_spd, z_spd) do
    call = {:move_absolute, [vec3, x_spd, y_spd, z_spd]}
    GenStage.call(__MODULE__, call, @call_timeout)
  end

  @doc "Calibrate an axis."
  def calibrate(axis) do
    GenStage.call(__MODULE__, {:calibrate, [axis]}, @call_timeout)
  end

  @doc "Find home on an axis."
  def find_home(axis) do
    GenStage.call(__MODULE__, {:find_home, [axis]}, @call_timeout)
  end

  @doc "Home every axis."
  def home_all() do
    GenStage.call(__MODULE__, {:home_all, []}, @call_timeout)
  end

  @doc "Home an axis."
  def home(axis) do
    GenStage.call(__MODULE__, {:home, [axis]}, @call_timeout)
  end

  @doc "Manually set an axis's current position to zero."
  def zero(axis) do
    GenStage.call(__MODULE__, {:zero, [axis]}, @call_timeout)
  end

  @doc """
  Update a paramater.
  For a list of paramaters see `Farmbot.Firmware.Gcode.Param`
  """
  def update_param(param, val) do
    GenStage.call(__MODULE__, {:update_param, [param, val]}, @call_timeout)
  end

  @doc false
  def read_all_params do
    GenStage.call(__MODULE__, {:read_all_params, []}, @call_timeout)
  end

  @doc """
  Read a paramater.
  For a list of paramaters see `Farmbot.Firmware.Gcode.Param`
  """
  def read_param(param) do
    GenStage.call(__MODULE__, {:read_param, [param]}, @call_timeout)
  end

  @doc "Emergency lock Farmbot."
  def emergency_lock() do
    GenStage.call(__MODULE__, {:emergency_lock, []}, @call_timeout)
  end

  @doc "Unlock Farmbot from Emergency state."
  def emergency_unlock() do
    GenStage.call(__MODULE__, {:emergency_unlock, []}, @call_timeout)
  end

  @doc "Set a pin mode (`:input` | `:output` | `:input_pullup`)"
  def set_pin_mode(pin, mode) do
    GenStage.call(__MODULE__, {:set_pin_mode, [pin, mode]}, @call_timeout)
  end

  @doc "Read a pin."
  def read_pin(pin, mode) do
    GenStage.call(__MODULE__, {:read_pin, [pin, mode]}, @call_timeout)
  end

  @doc "Write a pin."
  def write_pin(pin, mode, value) do
    GenStage.call(__MODULE__, {:write_pin, [pin, mode, value]}, @call_timeout)
  end

  @doc "Request version."
  def request_software_version do
    GenStage.call(__MODULE__, {:request_software_version, []}, @call_timeout)
  end

  @doc "Set angle of a servo pin."
  def set_servo_angle(pin, value) do
    GenStage.call(__MODULE__, {:set_servo_angle, [pin, value]}, @call_timeout)
  end

  @doc "Flag for all params reported."
  def params_reported do
    GenStage.call(__MODULE__, :params_reported)
  end

  @doc "Start the firmware services."
  def start_link do
    GenStage.start_link(__MODULE__, [], name: __MODULE__)
  end

  ## GenStage

  defmodule State do
    @moduledoc false
    defstruct [
      handler: nil,
      handler_mod: nil,
      idle: false,
      timer: nil,
      pins: %{},
      params: %{},
      params_reported: false,
      initialized: false,
      initializing: false,
      current: nil,
      timeout_ms: 150_000,
      queue: :queue.new(),
      x_needs_home_on_boot: false,
      y_needs_home_on_boot: false,
      z_needs_home_on_boot: false
    ]
  end

  defp needs_home_on_boot do
    x = get_config_value(:float, "hardware_params", "movement_home_at_boot_x") || 0
    |> num_to_bool()

    y = get_config_value(:float, "hardware_params", "movement_home_at_boot_y") || 0
    |> num_to_bool()

    z = get_config_value(:float, "hardware_params", "movement_home_at_boot_z") || 0
    |> num_to_bool()

    %{
      x_needs_home_on_boot: x,
      y_needs_home_on_boot: y,
      z_needs_home_on_boot: z,
    }
  end

  def init([]) do
    handler_mod =
      Application.get_env(:farmbot, :behaviour)[:firmware_handler] || raise("No fw handler.")

    case handler_mod.start_link() do
      {:ok, handler} ->
        initial = Map.merge(needs_home_on_boot(), %{handler: handler, handler_mod: handler_mod})
        Process.flag(:trap_exit, true)
        {
          :producer_consumer,
          struct(State, initial),
          subscribe_to: [handler], dispatcher: GenStage.BroadcastDispatcher
        }
      {:error, reason} ->
        replace_firmware_handler(Farmbot.Firmware.StubHandler)
        Logger.error 1, "Failed to initialize firmware: #{inspect reason} Falling back to stub implementation."
        init([])
    end

  end

  def terminate(reason, state) do
    unless reason in [:normal, :shutdown] do
      replace_firmware_handler(Farmbot.Firmware.StubHandler)
    end

    unless :queue.is_empty(state.queue) do
      list = :queue.to_list(state.queue)
      for cmd <- list do
        :ok = do_reply(%{state | current: cmd}, {:error, reason})
      end
    end
  end

  def handle_info({:EXIT, _pid, :normal}, state) do
    {:stop, :normal, state}
  end

  def handle_info({:EXIT, _, reason}, state) do
    Logger.error 1, "Firmware handler: #{state.handler_mod} died: #{inspect reason}"
    case state.handler_mod.start_link() do
      {:ok, handler} ->
        new_state = %{state | handler: handler}
        {:noreply, [{:informational_settings, %{busy: false}}], %{new_state | initialized: false, idle: false}}
      err -> {:stop, err, %{state | handler: false}}
    end
  end

  # TODO(Connor): Put some sort of exit strategy here.
  # If a firmware command keeps timingout/failing, Farmbot OS just keeps trying
  # it. This can lead to infinate failures.
  def handle_info({:command_timeout, %Command{} = timeout_command}, state) do
    case state.current do
      # Check if this timeout is actually talking about the current command.
      ^timeout_command = current ->
        Logger.warn 1, "Timed out waiting for Firmware response. Retrying #{inspect current}) "
        case apply(state.handler_mod, current.fun, [state.handler | current.args]) do
          :ok ->
            timer = start_timer(current, state.timeout_ms)
            {:noreply, [], %{state | current: current, timer: timer}}
          {:error, _} = res ->
            do_reply(state, res)
            {:noreply, [], %{state | current: nil, queue: :queue.new()}}
        end

      # If this timeout was not talking about the current command
      %Command{} = current ->
        Logger.debug 3, "Got stray timeout for command: #{inspect current}"
        {:noreply, [], %{state | timer: nil}}

      # If there is no current command, we got a different kind of stray.
      # This is ok i guess.
      nil -> {:noreply, [], %{state | timer: nil}}

    end
  end

  def handle_call(:params_reported, _, state) do
    {:reply, state.params_reported, [], state}
  end

  def handle_call({fun, _}, _from, state = %{initialized: false})
  when fun not in  [:read_all_params, :update_param, :emergency_unlock, :emergency_lock, :request_software_version] do
    {:reply, {:error, :uninitialized}, [], state}
  end

  def handle_call({fun, args}, from, state) do
    next_current = struct(Command, from: from, fun: fun, args: args)
    current_current = state.current
    cond do
      fun == :emergency_lock ->
        if current_current do
          do_reply(state, {:error, :emergency_lock})
        end
        do_begin_cmd(next_current, state, [])
      match?(%Command{}, current_current) ->
        do_queue_cmd(next_current, state)
      is_nil(current_current) ->
        do_begin_cmd(next_current, state, [])
    end
  end

  defp do_begin_cmd(%Command{fun: fun, args: args, from: _from} = current, state, dispatch) do
    case apply(state.handler_mod, fun, [state.handler | args]) do
      :ok ->
        timer = start_timer(current, state.timeout_ms)
        if fun == :emergency_unlock do
          Farmbot.System.GPIO.Leds.led_status_ok()
          new_dispatch = [{:informational_settings,  %{busy: false, locked: false}} | dispatch]
          {:noreply, new_dispatch, %{state | current: current, timer: timer}}
        else
          {:noreply, dispatch, %{state | current: current, timer: timer}}
        end
      {:error, _} = res ->
        do_reply(%{state | current: current}, res)
        {:noreply, dispatch, %{state | current: nil}}
    end
  end

  defp do_queue_cmd(%Command{fun: _fun, args: _args, from: _from} = current, state) do
    # Logger.busy 3, "FW Queuing: #{fun}: #{inspect from}"
    new_q = :queue.in(current, state.queue)
    {:noreply, [], %{state | queue: new_q}}
  end

  def handle_events(gcodes, _from, state) do
    {diffs, state} = handle_gcodes(gcodes, state)
    # if after handling the current buffer of gcodes,
    # Try to start the next command in the queue if it exists.
    if List.last(gcodes) == :idle && state.current == nil do
      case :queue.out(state.queue) do
        {{:value, next_current}, new_queue} ->
          do_begin_cmd(next_current, %{state | queue: new_queue, current: next_current}, diffs)
        {:empty, queue} -> # nothing to do if the queue is empty.
          {:noreply, diffs, %{state | queue: queue}}
      end
    else
      {:noreply, diffs, state}
    end
  end

  defp handle_gcodes(codes, state, acc \\ [])

  defp handle_gcodes([], state, acc), do: {Enum.reverse(acc), state}

  defp handle_gcodes([code | rest], state, acc) do
    case handle_gcode(code, state) do
      {nil, new_state} -> handle_gcodes(rest, new_state, acc)
      {key, diff, new_state} -> handle_gcodes(rest, new_state, [{key, diff} | acc])
    end
  end

  defp handle_gcode({:debug_message, message}, state) do
    if get_config_value(:bool, "settings", "arduino_debug_messages") do
      Logger.debug 3, "Arduino debug message: #{message}"
    end
    {nil, state}
  end

  defp handle_gcode(code, state) when code in [:error, :invalid_command] do
    maybe_cancel_timer(state.timer, state.current)
    if state.current do
      Logger.error 1, "Got #{code} while executing `#{inspect state.current}`."
      do_reply(state, {:error, :firmware_error})
      {nil, %{state | current: nil}}
    else
      {nil, state}
    end
  end

  defp handle_gcode(:report_no_config, state) do
    Logger.busy 1, "Initializing Firmware."
    old = get_config_as_map()["hardware_params"]
    spawn __MODULE__, :do_read_params, [Map.delete(old, "param_version")]
    {nil, %{state | initialized: false, initializing: true}}
  end

  defp handle_gcode(:report_params_complete, state) do
    {nil, %{state | initialized: true, initializing: false}}
  end

  defp handle_gcode(:idle, %{initialized: false, initializing: false} = state) do
    Logger.busy 1, "Firmware not initialized yet. Waiting for R88 message."
    {nil, state}
  end

  defp handle_gcode(:idle, %{initialized: true, initializing: false, current: nil, z_needs_home_on_boot: true} = state) do
    Logger.info 2, "Bootup homing Z axis"
    spawn __MODULE__, :find_home, [:z]
    {nil, %{state | z_needs_home_on_boot: false}}
  end

  defp handle_gcode(:idle, %{initialized: true, initializing: false, current: nil, y_needs_home_on_boot: true} = state) do
    Logger.info 2, "Bootup homing Y axis"
    spawn __MODULE__, :find_home, [:y]
    {nil, %{state | y_needs_home_on_boot: false}}
  end

  defp handle_gcode(:idle, %{initialized: true, initializing: false, current: nil, x_needs_home_on_boot: true} = state) do
    Logger.info 2, "Bootup homing X axis"
    spawn __MODULE__, :find_home, [:x]
    {nil, %{state | x_needs_home_on_boot: false}}
  end

  defp handle_gcode(:idle, state) do
    maybe_cancel_timer(state.timer, state.current)
    Farmbot.BotState.set_busy(false)
    if state.current do
      # This might be a bug in the FW
      if state.current.fun in [:home, :home_all] do
        Logger.warn 1, "Got idle during home."
        timer = start_timer(state.current, state.timeout_ms)
        {nil, %{state | timer: timer}}
      else
        Logger.warn 1, "Got idle while executing a command."
        do_reply(state, {:error, :timeout})
        {:informational_settings, %{busy: false, locked: false}, %{state | current: nil, idle: true}}
      end
    else
      {:informational_settings, %{busy: false, locked: false}, %{state | idle: true}}
    end
  end

  defp handle_gcode({:report_current_position, x, y, z}, state) do
    {:location_data, %{position: %{x: round(x), y: round(y), z: round(z)}}, state}
  end

  defp handle_gcode({:report_encoder_position_scaled, x, y, z}, state) do
    {:location_data, %{scaled_encoders: %{x: x, y: y, z: z}}, state}
  end

  defp handle_gcode({:report_encoder_position_raw, x, y, z}, state) do
    {:location_data, %{raw_encoders: %{x: x, y: y, z: z}}, state}
  end

  defp handle_gcode({:report_end_stops, xa, xb, ya, yb, za, zb}, state) do
    diff = %{end_stops: %{xa: xa, xb: xb, ya: ya, yb: yb, za: za, zb: zb}}
    {:location_data, diff, state}
    {nil, state}
  end

  defp handle_gcode({:report_pin_mode, pin, mode_atom}, state) do
    # Logger.debug 3, "Got pin mode report: #{pin}: #{mode_atom}"
    mode = extract_pin_mode(mode_atom)
    case state.pins[pin] do
      %{mode: _, value: _} = pin_map ->
        {:pins, %{pin => %{pin_map | mode: mode}}, %{state | pins: %{state.pins | pin => %{pin_map | mode: mode}}}}
      nil ->
        {:pins, %{pin => %{mode: mode, value: -1}}, %{state | pins: Map.put(state.pins, pin, %{mode: mode, value: -1})}}
    end
  end

  defp handle_gcode({:report_pin_value, pin, value}, state) do
    # Logger.debug 3, "Got pin value report: #{pin}: #{value} old: #{inspect state.pins[pin]}"
    case state.pins[pin] do
      %{mode: _, value: _} = pin_map ->
        {:pins, %{pin => %{pin_map | value: value}}, %{state | pins: %{state.pins | pin => %{pin_map | value: value}}}}
      nil ->
        {:pins, %{pin => %{mode: nil, value: value}}, %{state | pins: Map.put(state.pins, pin, %{mode: nil, value: value})}}
    end
  end

  defp handle_gcode({:report_parameter_value, param, value}, state) when (value == -1) do
    maybe_update_param_from_report(to_string(param), nil)
    {:mcu_params, %{param => nil}, %{state | params: Map.put(state.params, param, value)}}
  end

  defp handle_gcode({:report_parameter_value, param, value}, state) when is_number(value) do
    maybe_update_param_from_report(to_string(param), value)
    {:mcu_params, %{param => value}, %{state | params: Map.put(state.params, param, value)}}
  end

  defp handle_gcode({:report_software_version, version}, state) do
    case String.last(version) do
      "F" ->
        update_config_value(:string, "settings", "firmware_hardware", "farmduino")
      "R" ->
        update_config_value(:string, "settings", "firmware_hardware", "arduino")
      "G" ->
        update_config_value(:string, "settings", "firmware_hardware", "farmduino_k14")
      _ -> :ok
    end
    {:informational_settings, %{firmware_version: version}, state}
  end

  defp handle_gcode(:report_axis_home_complete_x, state) do
    {nil, state}
  end

  defp handle_gcode(:report_axis_home_complete_y, state) do
    {nil, state}
  end

  defp handle_gcode(:report_axis_home_complete_z, state) do
    {nil, %{state | timer: nil}}
  end

  defp handle_gcode(:report_axis_timeout_x, state) do
    do_reply(state, {:error, :axis_timeout_x})
    {nil, %{state | timer: nil}}
  end

  defp handle_gcode(:report_axis_timeout_y, state) do
    do_reply(state, {:error, :axis_timeout_y})
    {nil, %{state | timer: nil}}
  end

  defp handle_gcode(:report_axis_timeout_z, state) do
    do_reply(state, {:error, :axis_timeout_z})
    {nil, %{state | timer: nil}}
  end

  defp handle_gcode({:report_axis_changed_x, _new_x} = msg, state) do
    new_current = Command.add_status(state.current, msg)
    {nil, %{state | current: new_current}}
  end

  defp handle_gcode({:report_axis_changed_y, _new_y} = msg, state) do
    new_current = Command.add_status(state.current, msg)
    {nil, %{state | current: new_current}}
  end

  defp handle_gcode({:report_axis_changed_z, _new_z} = msg, state) do
    new_current = Command.add_status(state.current, msg)
    {nil, %{state | current: new_current}}
  end

  defp handle_gcode(:busy, state) do
    Farmbot.BotState.set_busy(true)
    maybe_cancel_timer(state.timer, state.current)
    timer = if state.current do
      start_timer(state.current, state.timeout_ms)
    else
      nil
    end
    {:informational_settings, %{busy: true}, %{state | idle: false, timer: timer}}
  end

  defp handle_gcode(:done, state) do
    maybe_cancel_timer(state.timer, state.current)
    Farmbot.BotState.set_busy(false)
    if state.current do
      do_reply(state, :ok)
      {nil, %{state | current: nil}}
    else
      {nil, state}
    end
  end

  defp handle_gcode(:report_emergency_lock, state) do
    Farmbot.System.GPIO.Leds.led_status_err
    maybe_send_email()
    if state.current do
      do_reply(state, {:error, :emergency_lock})
      {:informational_settings, %{locked: true}, %{state | current: nil}}
    else
      {:informational_settings, %{locked: true}, state}
    end
  end

  defp handle_gcode({:report_calibration, axis, status}, state) do
    maybe_cancel_timer(state.timer, state.current)
    Logger.busy 1, "Axis #{axis} calibration: #{status}"
    {nil, state}
  end

  defp handle_gcode({:report_axis_calibration, param, val}, state) do
    spawn __MODULE__, :report_calibration_callback, [5, param, val]
    {nil, state}
  end

  defp handle_gcode(:noop, state) do
    {nil, state}
  end

  defp handle_gcode(:received, state) do
    {nil, state}
  end

  defp handle_gcode({:echo, _code}, state) do
    {nil, state}
  end

  defp handle_gcode(code, state) do
    Logger.warn(3, "unhandled code: #{inspect(code)}")
    {nil, state}
  end

  defp maybe_cancel_timer(nil, current_command) do
    if current_command do
      # Logger.debug 3, "[WEIRD] - No timer to cancel for command: #{inspect current_command}"
      :ok
    else
      # Logger.debug 3, "[PROBABLY OK] - No timer to cancel, and no command here."
      :ok
    end
  end

  defp maybe_cancel_timer(timer, _current_command) do
    if Process.read_timer(timer) do
      # Logger.debug 3, "[NORMAL] - Canceled timer: #{inspect timer} for command: #{inspect current_command}"
      Process.cancel_timer(timer)
      :ok
    else
      :ok
    end
  end

  defp maybe_update_param_from_report(param, val) when is_binary(param) do
    real_val = if val, do: (val / 1), else: nil
    # Logger.debug 3, "Firmware reported #{param} => #{val || -1}"
    update_config_value(:float, "hardware_params", to_string(param), real_val)
  end

  @doc false
  def do_read_params(old) when is_map(old) do
    for {key, float_val} <- old do
      cond do
        (float_val == -1) -> :ok
        is_nil(float_val) -> :ok
        is_number(float_val) ->
          val = round(float_val)
          :ok = update_param(:"#{key}", val)
      end
    end
    :ok = update_param(:param_use_eeprom, 0)
    :ok = update_param(:param_config_ok, 1)
    read_all_params()
    :ok = request_software_version()
  end

  @doc false
  def report_calibration_callback(tries, param, value)

  def report_calibration_callback(0, _param, _value) do
    :ok
  end

  def report_calibration_callback(tries, param, val) do
    case Farmbot.Firmware.update_param(param, val) do
      :ok ->
        str_param = to_string(param)
        case get_config_value(:float, "hardware_params", str_param) do
          ^val ->
            Logger.success 1, "Calibrated #{param}: #{val}"
            SettingsSync.upload_fw_kv(str_param, val)
            :ok
          _ -> report_calibration_callback(tries - 1, param, val)
        end
      {:error, reason} ->
        Logger.error 1, "Failed to set #{param}: #{val} (#{inspect reason})"
        report_calibration_callback(tries - 1, param, val)
    end
  end

  defp do_reply(state, reply) do
    maybe_cancel_timer(state.timer, state.current)
    maybe_log_complete(state.current, reply)
    case state.current do
      %Command{fun: :emergency_unlock, from: from} ->
        # i really don't want this to be here..
        EstopTimer.cancel_timer()
        :ok = GenServer.reply from, reply
      %Command{fun: :emergency_lock, from: from} ->
        :ok = GenServer.reply from, {:error, :emergency_lock}
      %Command{fun: _fun, from: from} ->
        # Logger.success 3, "FW Replying: #{fun}: #{inspect from}"
        :ok = GenServer.reply from, reply
      nil ->
        Logger.error 1, "FW Nothing to send reply: #{inspect reply} to!."
        :error
    end
  end

  defp maybe_send_email do
    if get_config_value(:bool, "settings", "email_on_estop") do
      if !EstopTimer.timer_active? do
        EstopTimer.start_timer()
      end
    end
  end

  defp start_timer(%Command{} = command, timeout) do
    Process.send_after(self(), {:command_timeout, command}, timeout)
  end
end


Do not show duplicate docs for definitions with defaults, closes #7121

# Convenience helpers for showing docs, specs, types
# and opening modules. Invoked directly from IEx.Helpers.
defmodule IEx.Introspection do
  @moduledoc false

  import IEx, only: [dont_display_result: 0]

  alias Kernel.Typespec

  @doc """
  Decomposes an introspection call into `{mod, fun, arity}`,
  `{mod, fun}` or `mod`.
  """
  @default_modules [IEx.Helpers, Kernel, Kernel.SpecialForms]

  def decompose({:/, _, [call, arity]} = term) do
    case Macro.decompose_call(call) do
      {_mod, :__info__, []} when arity == 1 ->
        {:{}, [], [Module, :__info__, 1]}

      {mod, fun, []} ->
        {:{}, [], [mod, fun, arity]}

      {fun, []} ->
        {:{}, [], [find_decompose_fun_arity(fun, arity), fun, arity]}

      _ ->
        term
    end
  end

  def decompose(call) do
    case Macro.decompose_call(call) do
      {_mod, :__info__, []} ->
        Macro.escape({Module, :__info__, 1})

      {mod, fun, []} ->
        {mod, fun}

      {fun, []} ->
        {find_decompose_fun(fun), fun}

      _ ->
        call
    end
  end

  defp find_decompose_fun(fun) do
    Enum.find(@default_modules, Kernel, fn mod ->
      Keyword.has_key?(mod.__info__(:functions), fun) or
        Keyword.has_key?(mod.__info__(:macros), fun)
    end)
  end

  defp find_decompose_fun_arity(fun, arity) do
    pair = {fun, arity}

    Enum.find(@default_modules, Kernel, fn mod ->
      pair in mod.__info__(:functions) or pair in mod.__info__(:macros)
    end)
  end

  @doc """
  Opens the given module, mfa, file/line, binary.
  """
  def open(module) when is_atom(module) do
    case open_mfa(module, :__info__, 1) do
      {source, nil, _} -> open(source)
      {_, tuple, _} -> open(tuple)
      :error -> puts_error("Could not open: #{inspect(module)}. Module is not available.")
    end

    dont_display_result()
  end

  def open({module, function}) when is_atom(module) and is_atom(function) do
    case open_mfa(module, function, :*) do
      {_, _, nil} ->
        puts_error(
          "Could not open: #{inspect(module)}.#{function}. Function/macro is not available."
        )

      {_, _, tuple} ->
        open(tuple)

      :error ->
        puts_error("Could not open: #{inspect(module)}.#{function}. Module is not available.")
    end

    dont_display_result()
  end

  def open({module, function, arity})
      when is_atom(module) and is_atom(function) and is_integer(arity) do
    case open_mfa(module, function, arity) do
      {_, _, nil} ->
        puts_error(
          "Could not open: #{inspect(module)}.#{function}/#{arity}. Function/macro is not available."
        )

      {_, _, tuple} ->
        open(tuple)

      :error ->
        puts_error(
          "Could not open: #{inspect(module)}.#{function}/#{arity}. Module is not available."
        )
    end

    dont_display_result()
  end

  def open({file, line}) when is_binary(file) and is_integer(line) do
    cond do
      not File.regular?(file) ->
        puts_error("Could not open: #{inspect(file)}. File is not available.")

      editor = System.get_env("ELIXIR_EDITOR") || System.get_env("EDITOR") ->
        command =
          if editor =~ "__FILE__" or editor =~ "__LINE__" do
            editor
            |> String.replace("__FILE__", inspect(file))
            |> String.replace("__LINE__", Integer.to_string(line))
          else
            "#{editor} #{inspect(file)}:#{line}"
          end

        IO.write(IEx.color(:eval_info, :os.cmd(String.to_charlist(command))))

      true ->
        puts_error(
          "Could not open: #{inspect(file)}. " <>
            "Please set the ELIXIR_EDITOR or EDITOR environment variables with the " <>
            "command line invocation of your favorite EDITOR."
        )
    end

    dont_display_result()
  end

  def open(invalid) do
    puts_error("Invalid arguments for open helper: #{inspect(invalid)}")
    dont_display_result()
  end

  defp open_mfa(module, fun, arity) do
    with {:module, _} <- Code.ensure_loaded(module),
         source when is_list(source) <- module.module_info(:compile)[:source] do
      source = rewrite_source(module, source)
      open_abstract_code(module, fun, arity, source)
    else
      _ -> :error
    end
  end

  defp open_abstract_code(module, fun, arity, source) do
    fun = Atom.to_string(fun)

    with [_ | _] = beam <- :code.which(module),
         {:ok, {_, [abstract_code: abstract_code]}} <- :beam_lib.chunks(beam, [:abstract_code]),
         {:raw_abstract_v1, code} <- abstract_code do
      {_, module_pair, fa_pair} =
        Enum.reduce(code, {source, nil, nil}, &open_abstract_code_reduce(&1, &2, fun, arity))

      {source, module_pair, fa_pair}
    else
      _ ->
        {source, nil, nil}
    end
  end

  defp open_abstract_code_reduce(entry, {file, module_pair, fa_pair}, fun, arity) do
    case entry do
      {:attribute, ann, :module, _} ->
        {file, {file, :erl_anno.line(ann)}, fa_pair}

      {:function, ann, ann_fun, ann_arity, _} ->
        case Atom.to_string(ann_fun) do
          "MACRO-" <> ^fun when arity == :* or ann_arity == arity + 1 ->
            {file, module_pair, fa_pair || {file, :erl_anno.line(ann)}}

          ^fun when arity == :* or ann_arity == arity ->
            {file, module_pair, fa_pair || {file, :erl_anno.line(ann)}}

          _ ->
            {file, module_pair, fa_pair}
        end

      _ ->
        {file, module_pair, fa_pair}
    end
  end

  @elixir_apps ~w(eex elixir ex_unit iex logger mix)a
  @otp_apps ~w(kernel stdlib)a
  @apps @elixir_apps ++ @otp_apps

  defp rewrite_source(module, source) do
    case :application.get_application(module) do
      {:ok, app} when app in @apps ->
        Application.app_dir(app, rewrite_source(source))

      _ ->
        beam_path = :code.which(module)

        if is_list(beam_path) and List.starts_with?(beam_path, :code.root_dir()) do
          app_vsn = beam_path |> Path.dirname() |> Path.dirname() |> Path.basename()
          Path.join([:code.root_dir(), "lib", app_vsn, rewrite_source(source)])
        else
          List.to_string(source)
        end
    end
  end

  defp rewrite_source(source) do
    {in_app, [lib_or_src | _]} =
      source
      |> Path.split()
      |> Enum.reverse()
      |> Enum.split_while(&(&1 not in ["lib", "src"]))

    Path.join([lib_or_src | Enum.reverse(in_app)])
  end

  @doc """
  Prints documentation.
  """
  def h(module) when is_atom(module) do
    case Code.ensure_loaded(module) do
      {:module, _} ->
        if elixir_module?(module) do
          case Code.get_docs(module, :moduledoc) do
            {_, binary} when is_binary(binary) ->
              print_doc(inspect(module), [], binary)

            {_, _} ->
              docs_not_found(inspect(module))

            _ ->
              no_docs(module)
          end
        else
          puts_error(
            "Documentation is not available for non-Elixir modules, got: #{inspect(module)}"
          )
        end

      {:error, reason} ->
        puts_error("Could not load module #{inspect(module)}, got: #{reason}")
    end

    dont_display_result()
  end

  def h({module, function}) when is_atom(module) and is_atom(function) do
    case Code.ensure_loaded(module) do
      {:module, _} ->
        docs = Code.get_docs(module, :docs)

        exports =
          cond do
            docs ->
              Enum.map(docs, &elem(&1, 0))

            function_exported?(module, :__info__, 1) ->
              module.__info__(:functions) ++ module.__info__(:macros)

            true ->
              module.module_info(:exports)
          end

        result =
          for {^function, arity} <- exports,
              (if docs do
                 find_doc(docs, function, arity)
               else
                 get_spec(module, function, arity) != []
               end) do
            h_mod_fun_arity(module, function, arity)
          end

        cond do
          result != [] ->
            :ok

          docs && has_callback?(module, function) ->
            behaviour_found("#{inspect(module)}.#{function}")

          elixir_module?(module) and is_nil(docs) ->
            no_docs(module)

          true ->
            docs_not_found("#{inspect(module)}.#{function}")
        end

      {:error, reason} ->
        puts_error("Could not load module #{inspect(module)}, got: #{reason}")
    end

    dont_display_result()
  end

  def h({module, function, arity})
      when is_atom(module) and is_atom(function) and is_integer(arity) do
    case Code.ensure_loaded(module) do
      {:module, _} ->
        case h_mod_fun_arity(module, function, arity) do
          :ok ->
            :ok

          :behaviour_found ->
            behaviour_found("#{inspect(module)}.#{function}/#{arity}")

          :no_docs ->
            no_docs(module)

          :not_found ->
            docs_not_found("#{inspect(module)}.#{function}/#{arity}")
        end

      {:error, reason} ->
        puts_error("Could not load module #{inspect(module)}, got: #{reason}")
    end

    dont_display_result()
  end

  def h(invalid) do
    puts_error("Invalid arguments for h helper: #{inspect(invalid)}")
    dont_display_result()
  end

  defp h_mod_fun_arity(mod, fun, arity) when is_atom(mod) do
    docs = Code.get_docs(mod, :docs)
    spec = get_spec(mod, fun, arity)

    cond do
      doc_tuple = find_doc(docs, fun, arity) ->
        print_fun(mod, doc_tuple, spec)
        :ok

      docs && has_callback?(mod, fun, arity) ->
        :behaviour_found

      is_nil(docs) and spec != [] ->
        message =
          if elixir_module?(mod) do
            "Module was compiled without docs. Showing only specs."
          else
            "Documentation is not available for non-Elixir modules. Showing only specs."
          end

        print_doc("#{inspect(mod)}.#{fun}/#{arity}", spec, message)
        :ok

      is_nil(docs) and elixir_module?(mod) ->
        :no_docs

      true ->
        :not_found
    end
  end

  defp has_callback?(mod, fun) do
    mod
    |> Code.get_docs(:callback_docs)
    |> Enum.any?(&match?({{^fun, _}, _, _, _}, &1))
  end

  defp has_callback?(mod, fun, arity) do
    mod
    |> Code.get_docs(:callback_docs)
    |> Enum.any?(&match?({{^fun, ^arity}, _, _, _}, &1))
  end

  defp find_doc(nil, _fun, _arity) do
    nil
  end

  defp find_doc(docs, fun, arity) do
    doc = List.keyfind(docs, {fun, arity}, 0) || find_doc_defaults(docs, fun, arity)
    if doc != nil and has_content?(doc), do: doc
  end

  defp find_doc_defaults(docs, function, min) do
    Enum.find(docs, fn doc ->
      case elem(doc, 0) do
        {^function, arity} when arity > min ->
          defaults = Enum.count(elem(doc, 3), &match?({:\\, _, _}, &1))
          arity <= min + defaults

        _ ->
          false
      end
    end)
  end

  defp has_content?({_, _, _, _, false}), do: false
  defp has_content?({{name, _}, _, _, _, nil}), do: hd(Atom.to_charlist(name)) != ?_
  defp has_content?({_, _, _, _, _}), do: true

  defp print_fun(mod, {{fun, arity}, _line, kind, args, doc}, spec) do
    if callback_module = is_nil(doc) and callback_module(mod, fun, arity) do
      filter = &match?({^fun, ^arity}, elem(&1, 0))

      case get_callback_docs(callback_module, filter) do
        {:ok, callback_docs} -> Enum.each(callback_docs, &print_typespec/1)
        _ -> nil
      end
    else
      args = Enum.map_join(args, ", ", &format_doc_arg(&1))
      print_doc("#{kind} #{fun}(#{args})", spec, doc)
    end
  end

  defp callback_module(mod, fun, arity) do
    predicate = &match?({{^fun, ^arity}, _}, &1)

    mod.module_info(:attributes)
    |> Keyword.get_values(:behaviour)
    |> Stream.concat()
    |> Enum.find(&Enum.any?(Typespec.beam_callbacks(&1), predicate))
  end

  defp format_doc_arg({:\\, _, [left, right]}) do
    format_doc_arg(left) <> " \\\\ " <> Macro.to_string(right)
  end

  defp format_doc_arg({var, _, _}) do
    Atom.to_string(var)
  end

  defp get_spec(module, name, arity) do
    all_specs = Typespec.beam_specs(module) || []

    case List.keyfind(all_specs, {name, arity}, 0) do
      {_, specs} ->
        formatted =
          Enum.map(specs, fn spec ->
            Typespec.spec_to_ast(name, spec)
            |> format_typespec(:spec)
            |> IO.iodata_to_binary()
            |> String.replace("\n", "\n    ")
            |> prefix("    ")
            |> pair(?\n)
          end)

        [formatted, ?\n]

      nil ->
        []
    end
  end

  @doc """
  Prints the list of behaviour callbacks or a given callback.
  """
  def b(mod) when is_atom(mod) do
    case get_callback_docs(mod, fn _ -> true end) do
      :no_beam -> no_beam(mod)
      :no_docs -> no_docs(mod)
      {:ok, []} -> puts_error("No callbacks for #{inspect(mod)} were found")
      {:ok, docs} -> Enum.each(docs, fn {definition, _} -> IO.puts(definition) end)
    end

    dont_display_result()
  end

  def b({mod, fun}) when is_atom(mod) and is_atom(fun) do
    filter = &match?({^fun, _}, elem(&1, 0))

    case get_callback_docs(mod, filter) do
      :no_beam -> no_beam(mod)
      :no_docs -> no_docs(mod)
      {:ok, []} -> docs_not_found("#{inspect(mod)}.#{fun}")
      {:ok, docs} -> Enum.each(docs, &print_typespec/1)
    end

    dont_display_result()
  end

  def b({mod, fun, arity}) when is_atom(mod) and is_atom(fun) and is_integer(arity) do
    filter = &match?({^fun, ^arity}, elem(&1, 0))

    case get_callback_docs(mod, filter) do
      :no_beam -> no_beam(mod)
      :no_docs -> no_docs(mod)
      {:ok, []} -> docs_not_found("#{inspect(mod)}.#{fun}/#{arity}")
      {:ok, docs} -> Enum.each(docs, &print_typespec/1)
    end

    dont_display_result()
  end

  def b(invalid) do
    puts_error("Invalid arguments for b helper: #{inspect(invalid)}")
    dont_display_result()
  end

  defp get_callback_docs(mod, filter) do
    callbacks = Typespec.beam_callbacks(mod)
    docs = Code.get_docs(mod, :callback_docs)

    cond do
      is_nil(callbacks) ->
        :no_beam

      is_nil(docs) ->
        :no_docs

      true ->
        docs =
          docs
          |> Enum.filter(filter)
          |> Enum.map(fn
            {{fun, arity}, _, :macrocallback, doc} ->
              macro = {:"MACRO-#{fun}", arity + 1}
              {format_callback(:macrocallback, fun, macro, callbacks), doc}

            {{fun, arity}, _, kind, doc} ->
              {format_callback(kind, fun, {fun, arity}, callbacks), doc}
          end)

        {:ok, docs}
    end
  end

  defp format_callback(kind, name, key, callbacks) do
    {_, specs} = List.keyfind(callbacks, key, 0)

    Enum.map(specs, fn spec ->
      Typespec.spec_to_ast(name, spec)
      |> Macro.prewalk(&drop_macro_env/1)
      |> format_typespec(kind)
      |> pair(?\n)
    end)
  end

  defp drop_macro_env({name, meta, [{:::, _, [_, {{:., _, [Macro.Env, :t]}, _, _}]} | args]}),
    do: {name, meta, args}

  defp drop_macro_env(other), do: other

  @doc """
  Prints the types for the given module and type documentation.
  """
  def t(module) when is_atom(module) do
    case Typespec.beam_types(module) do
      nil ->
        no_beam(module)

      [] ->
        types_not_found(inspect(module))

      types ->
        Enum.each(types, &(&1 |> format_type() |> IO.puts()))
    end

    dont_display_result()
  end

  def t({module, type}) when is_atom(module) and is_atom(type) do
    case Typespec.beam_types(module) do
      nil ->
        no_beam(module)

      types ->
        printed =
          for {_, {^type, _, args}} = typespec <- types do
            doc = {format_type(typespec), type_doc(module, type, length(args))}
            print_typespec(doc)
          end

        if printed == [] do
          types_not_found("#{inspect(module)}.#{type}")
        end
    end

    dont_display_result()
  end

  def t({module, type, arity}) when is_atom(module) and is_atom(type) and is_integer(arity) do
    case Typespec.beam_types(module) do
      nil ->
        no_beam(module)

      types ->
        printed =
          for {_, {^type, _, args}} = typespec <- types, length(args) == arity do
            doc = {format_type(typespec), type_doc(module, type, arity)}
            print_typespec(doc)
          end

        if printed == [] do
          types_not_found("#{inspect(module)}.#{type}")
        end
    end

    dont_display_result()
  end

  def t(invalid) do
    puts_error("Invalid arguments for t helper: #{inspect(invalid)}")
    dont_display_result()
  end

  defp type_doc(module, type, arity) do
    docs = Code.get_docs(module, :type_docs)
    {_, _, _, content} = Enum.find(docs, &match?({{^type, ^arity}, _, _, _}, &1))
    content
  end

  defp format_type({:opaque, type}) do
    {:::, _, [ast, _]} = Typespec.type_to_ast(type)
    [format_typespec(ast, :opaque), ?\n]
  end

  defp format_type({kind, type}) do
    ast = Typespec.type_to_ast(type)
    [format_typespec(ast, kind), ?\n]
  end

  ## Helpers

  defp format_typespec(definition, kind) do
    definition
    |> Macro.to_string()
    |> prefix("@#{kind} ")
    |> Code.format_string!(line_length: IEx.width())
    |> IO.iodata_to_binary()
    |> color_prefix_with_line()
  end

  defp prefix(string, prefix) do
    prefix <> string
  end

  defp pair(left, right) do
    [left, right]
  end

  defp color_prefix_with_line(string) do
    [left, right] = :binary.split(string, " ")
    [IEx.color(:doc_inline_code, left), ?\s, right]
  end

  defp print_doc(heading, types, doc) do
    doc = doc || ""

    if opts = IEx.Config.ansi_docs() do
      IO.ANSI.Docs.print_heading(heading, opts)
      IO.write(types)
      IO.ANSI.Docs.print(doc, opts)
    else
      IO.puts("* #{heading}\n")
      IO.write(types)
      IO.puts(doc)
    end
  end

  defp print_typespec({types, doc}) do
    IO.puts(types)

    if opts = IEx.Config.ansi_docs() do
      doc && IO.ANSI.Docs.print(doc, opts)
    else
      doc && IO.puts(doc)
    end
  end

  defp elixir_module?(module) do
    function_exported?(module, :__info__, 1)
  end

  defp no_beam(module) do
    case Code.ensure_loaded(module) do
      {:module, _} ->
        puts_error(
          "Beam code not available for #{inspect(module)} or debug info is missing, cannot load typespecs"
        )

      {:error, reason} ->
        puts_error("Could not load module #{inspect(module)}, got: #{reason}")
    end
  end

  defp no_docs(module) do
    puts_error("#{inspect(module)} was not compiled with docs")
  end

  defp types_not_found(for), do: not_found(for, "type information")
  defp docs_not_found(for), do: not_found(for, "documentation")

  defp behaviour_found(for) do
    puts_error("""
    No documentation for function #{for} was found, but there is a callback with the same name.
    You can view callback documentations with the b/1 helper.
    """)
  end

  defp not_found(for, type) do
    puts_error("No #{type} for #{for} was found")
  end

  defp puts_error(string) do
    IO.puts(IEx.color(:eval_error, string))
  end
end

# Convenience helpers for showing docs, specs, types
# and opening modules. Invoked directly from IEx.Helpers.
defmodule IEx.Introspection do
  @moduledoc false

  import IEx, only: [dont_display_result: 0]

  alias Kernel.Typespec

  @doc """
  Decomposes an introspection call into `{mod, fun, arity}`,
  `{mod, fun}` or `mod`.
  """
  @default_modules [IEx.Helpers, Kernel, Kernel.SpecialForms]

  def decompose({:/, _, [call, arity]} = term) do
    case Macro.decompose_call(call) do
      {_mod, :__info__, []} when arity == 1 ->
        {:{}, [], [Module, :__info__, 1]}

      {mod, fun, []} ->
        {:{}, [], [mod, fun, arity]}

      {fun, []} ->
        {:{}, [], [find_decompose_fun_arity(fun, arity), fun, arity]}

      _ ->
        term
    end
  end

  def decompose(call) do
    case Macro.decompose_call(call) do
      {_mod, :__info__, []} ->
        Macro.escape({Module, :__info__, 1})

      {mod, fun, []} ->
        {mod, fun}

      {fun, []} ->
        {find_decompose_fun(fun), fun}

      _ ->
        call
    end
  end

  defp find_decompose_fun(fun) do
    Enum.find(@default_modules, Kernel, fn mod ->
      Keyword.has_key?(mod.__info__(:functions), fun) or
        Keyword.has_key?(mod.__info__(:macros), fun)
    end)
  end

  defp find_decompose_fun_arity(fun, arity) do
    pair = {fun, arity}

    Enum.find(@default_modules, Kernel, fn mod ->
      pair in mod.__info__(:functions) or pair in mod.__info__(:macros)
    end)
  end

  @doc """
  Opens the given module, mfa, file/line, binary.
  """
  def open(module) when is_atom(module) do
    case open_mfa(module, :__info__, 1) do
      {source, nil, _} -> open(source)
      {_, tuple, _} -> open(tuple)
      :error -> puts_error("Could not open: #{inspect(module)}. Module is not available.")
    end

    dont_display_result()
  end

  def open({module, function}) when is_atom(module) and is_atom(function) do
    case open_mfa(module, function, :*) do
      {_, _, nil} ->
        puts_error(
          "Could not open: #{inspect(module)}.#{function}. Function/macro is not available."
        )

      {_, _, tuple} ->
        open(tuple)

      :error ->
        puts_error("Could not open: #{inspect(module)}.#{function}. Module is not available.")
    end

    dont_display_result()
  end

  def open({module, function, arity})
      when is_atom(module) and is_atom(function) and is_integer(arity) do
    case open_mfa(module, function, arity) do
      {_, _, nil} ->
        puts_error(
          "Could not open: #{inspect(module)}.#{function}/#{arity}. Function/macro is not available."
        )

      {_, _, tuple} ->
        open(tuple)

      :error ->
        puts_error(
          "Could not open: #{inspect(module)}.#{function}/#{arity}. Module is not available."
        )
    end

    dont_display_result()
  end

  def open({file, line}) when is_binary(file) and is_integer(line) do
    cond do
      not File.regular?(file) ->
        puts_error("Could not open: #{inspect(file)}. File is not available.")

      editor = System.get_env("ELIXIR_EDITOR") || System.get_env("EDITOR") ->
        command =
          if editor =~ "__FILE__" or editor =~ "__LINE__" do
            editor
            |> String.replace("__FILE__", inspect(file))
            |> String.replace("__LINE__", Integer.to_string(line))
          else
            "#{editor} #{inspect(file)}:#{line}"
          end

        IO.write(IEx.color(:eval_info, :os.cmd(String.to_charlist(command))))

      true ->
        puts_error(
          "Could not open: #{inspect(file)}. " <>
            "Please set the ELIXIR_EDITOR or EDITOR environment variables with the " <>
            "command line invocation of your favorite EDITOR."
        )
    end

    dont_display_result()
  end

  def open(invalid) do
    puts_error("Invalid arguments for open helper: #{inspect(invalid)}")
    dont_display_result()
  end

  defp open_mfa(module, fun, arity) do
    with {:module, _} <- Code.ensure_loaded(module),
         source when is_list(source) <- module.module_info(:compile)[:source] do
      source = rewrite_source(module, source)
      open_abstract_code(module, fun, arity, source)
    else
      _ -> :error
    end
  end

  defp open_abstract_code(module, fun, arity, source) do
    fun = Atom.to_string(fun)

    with [_ | _] = beam <- :code.which(module),
         {:ok, {_, [abstract_code: abstract_code]}} <- :beam_lib.chunks(beam, [:abstract_code]),
         {:raw_abstract_v1, code} <- abstract_code do
      {_, module_pair, fa_pair} =
        Enum.reduce(code, {source, nil, nil}, &open_abstract_code_reduce(&1, &2, fun, arity))

      {source, module_pair, fa_pair}
    else
      _ ->
        {source, nil, nil}
    end
  end

  defp open_abstract_code_reduce(entry, {file, module_pair, fa_pair}, fun, arity) do
    case entry do
      {:attribute, ann, :module, _} ->
        {file, {file, :erl_anno.line(ann)}, fa_pair}

      {:function, ann, ann_fun, ann_arity, _} ->
        case Atom.to_string(ann_fun) do
          "MACRO-" <> ^fun when arity == :* or ann_arity == arity + 1 ->
            {file, module_pair, fa_pair || {file, :erl_anno.line(ann)}}

          ^fun when arity == :* or ann_arity == arity ->
            {file, module_pair, fa_pair || {file, :erl_anno.line(ann)}}

          _ ->
            {file, module_pair, fa_pair}
        end

      _ ->
        {file, module_pair, fa_pair}
    end
  end

  @elixir_apps ~w(eex elixir ex_unit iex logger mix)a
  @otp_apps ~w(kernel stdlib)a
  @apps @elixir_apps ++ @otp_apps

  defp rewrite_source(module, source) do
    case :application.get_application(module) do
      {:ok, app} when app in @apps ->
        Application.app_dir(app, rewrite_source(source))

      _ ->
        beam_path = :code.which(module)

        if is_list(beam_path) and List.starts_with?(beam_path, :code.root_dir()) do
          app_vsn = beam_path |> Path.dirname() |> Path.dirname() |> Path.basename()
          Path.join([:code.root_dir(), "lib", app_vsn, rewrite_source(source)])
        else
          List.to_string(source)
        end
    end
  end

  defp rewrite_source(source) do
    {in_app, [lib_or_src | _]} =
      source
      |> Path.split()
      |> Enum.reverse()
      |> Enum.split_while(&(&1 not in ["lib", "src"]))

    Path.join([lib_or_src | Enum.reverse(in_app)])
  end

  @doc """
  Prints documentation.
  """
  def h(module) when is_atom(module) do
    case Code.ensure_loaded(module) do
      {:module, _} ->
        if elixir_module?(module) do
          case Code.get_docs(module, :moduledoc) do
            {_, binary} when is_binary(binary) ->
              print_doc(inspect(module), [], binary)

            {_, _} ->
              docs_not_found(inspect(module))

            _ ->
              no_docs(module)
          end
        else
          puts_error(
            "Documentation is not available for non-Elixir modules, got: #{inspect(module)}"
          )
        end

      {:error, reason} ->
        puts_error("Could not load module #{inspect(module)}, got: #{reason}")
    end

    dont_display_result()
  end

  def h({module, function}) when is_atom(module) and is_atom(function) do
    case Code.ensure_loaded(module) do
      {:module, _} ->
        docs = Code.get_docs(module, :docs)

        exports =
          if function_exported?(module, :__info__, 1) do
            module.__info__(:functions) ++ module.__info__(:macros)
          else
            module.module_info(:exports)
          end

        result =
          for {^function, arity} <- exports,
              (if docs do
                 find_doc(docs, function, arity)
               else
                 get_spec(module, function, arity) != []
               end) do
            h_mod_fun_arity(module, function, arity)
          end

        cond do
          result != [] ->
            :ok

          docs && has_callback?(module, function) ->
            behaviour_found("#{inspect(module)}.#{function}")

          elixir_module?(module) and is_nil(docs) ->
            no_docs(module)

          true ->
            docs_not_found("#{inspect(module)}.#{function}")
        end

      {:error, reason} ->
        puts_error("Could not load module #{inspect(module)}, got: #{reason}")
    end

    dont_display_result()
  end

  def h({module, function, arity})
      when is_atom(module) and is_atom(function) and is_integer(arity) do
    case Code.ensure_loaded(module) do
      {:module, _} ->
        case h_mod_fun_arity(module, function, arity) do
          :ok ->
            :ok

          :behaviour_found ->
            behaviour_found("#{inspect(module)}.#{function}/#{arity}")

          :no_docs ->
            no_docs(module)

          :not_found ->
            docs_not_found("#{inspect(module)}.#{function}/#{arity}")
        end

      {:error, reason} ->
        puts_error("Could not load module #{inspect(module)}, got: #{reason}")
    end

    dont_display_result()
  end

  def h(invalid) do
    puts_error("Invalid arguments for h helper: #{inspect(invalid)}")
    dont_display_result()
  end

  defp h_mod_fun_arity(mod, fun, arity) when is_atom(mod) do
    docs = Code.get_docs(mod, :docs)
    spec = get_spec(mod, fun, arity)

    cond do
      doc_tuple = find_doc(docs, fun, arity) ->
        print_fun(mod, doc_tuple, spec)
        :ok

      docs && has_callback?(mod, fun, arity) ->
        :behaviour_found

      is_nil(docs) and spec != [] ->
        message =
          if elixir_module?(mod) do
            "Module was compiled without docs. Showing only specs."
          else
            "Documentation is not available for non-Elixir modules. Showing only specs."
          end

        print_doc("#{inspect(mod)}.#{fun}/#{arity}", spec, message)
        :ok

      is_nil(docs) and elixir_module?(mod) ->
        :no_docs

      true ->
        :not_found
    end
  end

  defp has_callback?(mod, fun) do
    mod
    |> Code.get_docs(:callback_docs)
    |> Enum.any?(&match?({{^fun, _}, _, _, _}, &1))
  end

  defp has_callback?(mod, fun, arity) do
    mod
    |> Code.get_docs(:callback_docs)
    |> Enum.any?(&match?({{^fun, ^arity}, _, _, _}, &1))
  end

  defp find_doc(nil, _fun, _arity) do
    nil
  end

  defp find_doc(docs, fun, arity) do
    doc = List.keyfind(docs, {fun, arity}, 0) || find_doc_defaults(docs, fun, arity)
    if doc != nil and has_content?(doc), do: doc
  end

  defp find_doc_defaults(docs, function, min) do
    Enum.find(docs, fn doc ->
      case elem(doc, 0) do
        {^function, arity} when arity > min ->
          defaults = Enum.count(elem(doc, 3), &match?({:\\, _, _}, &1))
          arity <= min + defaults

        _ ->
          false
      end
    end)
  end

  defp has_content?({_, _, _, _, false}), do: false
  defp has_content?({{name, _}, _, _, _, nil}), do: hd(Atom.to_charlist(name)) != ?_
  defp has_content?({_, _, _, _, _}), do: true

  defp print_fun(mod, {{fun, arity}, _line, kind, args, doc}, spec) do
    if callback_module = is_nil(doc) and callback_module(mod, fun, arity) do
      filter = &match?({^fun, ^arity}, elem(&1, 0))

      case get_callback_docs(callback_module, filter) do
        {:ok, callback_docs} -> Enum.each(callback_docs, &print_typespec/1)
        _ -> nil
      end
    else
      args = Enum.map_join(args, ", ", &format_doc_arg(&1))
      print_doc("#{kind} #{fun}(#{args})", spec, doc)
    end
  end

  defp callback_module(mod, fun, arity) do
    predicate = &match?({{^fun, ^arity}, _}, &1)

    mod.module_info(:attributes)
    |> Keyword.get_values(:behaviour)
    |> Stream.concat()
    |> Enum.find(&Enum.any?(Typespec.beam_callbacks(&1), predicate))
  end

  defp format_doc_arg({:\\, _, [left, right]}) do
    format_doc_arg(left) <> " \\\\ " <> Macro.to_string(right)
  end

  defp format_doc_arg({var, _, _}) do
    Atom.to_string(var)
  end

  defp get_spec(module, name, arity) do
    all_specs = Typespec.beam_specs(module) || []

    case List.keyfind(all_specs, {name, arity}, 0) do
      {_, specs} ->
        formatted =
          Enum.map(specs, fn spec ->
            Typespec.spec_to_ast(name, spec)
            |> format_typespec(:spec)
            |> IO.iodata_to_binary()
            |> String.replace("\n", "\n    ")
            |> prefix("    ")
            |> pair(?\n)
          end)

        [formatted, ?\n]

      nil ->
        []
    end
  end

  @doc """
  Prints the list of behaviour callbacks or a given callback.
  """
  def b(mod) when is_atom(mod) do
    case get_callback_docs(mod, fn _ -> true end) do
      :no_beam -> no_beam(mod)
      :no_docs -> no_docs(mod)
      {:ok, []} -> puts_error("No callbacks for #{inspect(mod)} were found")
      {:ok, docs} -> Enum.each(docs, fn {definition, _} -> IO.puts(definition) end)
    end

    dont_display_result()
  end

  def b({mod, fun}) when is_atom(mod) and is_atom(fun) do
    filter = &match?({^fun, _}, elem(&1, 0))

    case get_callback_docs(mod, filter) do
      :no_beam -> no_beam(mod)
      :no_docs -> no_docs(mod)
      {:ok, []} -> docs_not_found("#{inspect(mod)}.#{fun}")
      {:ok, docs} -> Enum.each(docs, &print_typespec/1)
    end

    dont_display_result()
  end

  def b({mod, fun, arity}) when is_atom(mod) and is_atom(fun) and is_integer(arity) do
    filter = &match?({^fun, ^arity}, elem(&1, 0))

    case get_callback_docs(mod, filter) do
      :no_beam -> no_beam(mod)
      :no_docs -> no_docs(mod)
      {:ok, []} -> docs_not_found("#{inspect(mod)}.#{fun}/#{arity}")
      {:ok, docs} -> Enum.each(docs, &print_typespec/1)
    end

    dont_display_result()
  end

  def b(invalid) do
    puts_error("Invalid arguments for b helper: #{inspect(invalid)}")
    dont_display_result()
  end

  defp get_callback_docs(mod, filter) do
    callbacks = Typespec.beam_callbacks(mod)
    docs = Code.get_docs(mod, :callback_docs)

    cond do
      is_nil(callbacks) ->
        :no_beam

      is_nil(docs) ->
        :no_docs

      true ->
        docs =
          docs
          |> Enum.filter(filter)
          |> Enum.map(fn
            {{fun, arity}, _, :macrocallback, doc} ->
              macro = {:"MACRO-#{fun}", arity + 1}
              {format_callback(:macrocallback, fun, macro, callbacks), doc}

            {{fun, arity}, _, kind, doc} ->
              {format_callback(kind, fun, {fun, arity}, callbacks), doc}
          end)

        {:ok, docs}
    end
  end

  defp format_callback(kind, name, key, callbacks) do
    {_, specs} = List.keyfind(callbacks, key, 0)

    Enum.map(specs, fn spec ->
      Typespec.spec_to_ast(name, spec)
      |> Macro.prewalk(&drop_macro_env/1)
      |> format_typespec(kind)
      |> pair(?\n)
    end)
  end

  defp drop_macro_env({name, meta, [{:::, _, [_, {{:., _, [Macro.Env, :t]}, _, _}]} | args]}),
    do: {name, meta, args}

  defp drop_macro_env(other), do: other

  @doc """
  Prints the types for the given module and type documentation.
  """
  def t(module) when is_atom(module) do
    case Typespec.beam_types(module) do
      nil ->
        no_beam(module)

      [] ->
        types_not_found(inspect(module))

      types ->
        Enum.each(types, &(&1 |> format_type() |> IO.puts()))
    end

    dont_display_result()
  end

  def t({module, type}) when is_atom(module) and is_atom(type) do
    case Typespec.beam_types(module) do
      nil ->
        no_beam(module)

      types ->
        printed =
          for {_, {^type, _, args}} = typespec <- types do
            doc = {format_type(typespec), type_doc(module, type, length(args))}
            print_typespec(doc)
          end

        if printed == [] do
          types_not_found("#{inspect(module)}.#{type}")
        end
    end

    dont_display_result()
  end

  def t({module, type, arity}) when is_atom(module) and is_atom(type) and is_integer(arity) do
    case Typespec.beam_types(module) do
      nil ->
        no_beam(module)

      types ->
        printed =
          for {_, {^type, _, args}} = typespec <- types, length(args) == arity do
            doc = {format_type(typespec), type_doc(module, type, arity)}
            print_typespec(doc)
          end

        if printed == [] do
          types_not_found("#{inspect(module)}.#{type}")
        end
    end

    dont_display_result()
  end

  def t(invalid) do
    puts_error("Invalid arguments for t helper: #{inspect(invalid)}")
    dont_display_result()
  end

  defp type_doc(module, type, arity) do
    docs = Code.get_docs(module, :type_docs)
    {_, _, _, content} = Enum.find(docs, &match?({{^type, ^arity}, _, _, _}, &1))
    content
  end

  defp format_type({:opaque, type}) do
    {:::, _, [ast, _]} = Typespec.type_to_ast(type)
    [format_typespec(ast, :opaque), ?\n]
  end

  defp format_type({kind, type}) do
    ast = Typespec.type_to_ast(type)
    [format_typespec(ast, kind), ?\n]
  end

  ## Helpers

  defp format_typespec(definition, kind) do
    definition
    |> Macro.to_string()
    |> prefix("@#{kind} ")
    |> Code.format_string!(line_length: IEx.width())
    |> IO.iodata_to_binary()
    |> color_prefix_with_line()
  end

  defp prefix(string, prefix) do
    prefix <> string
  end

  defp pair(left, right) do
    [left, right]
  end

  defp color_prefix_with_line(string) do
    [left, right] = :binary.split(string, " ")
    [IEx.color(:doc_inline_code, left), ?\s, right]
  end

  defp print_doc(heading, types, doc) do
    doc = doc || ""

    if opts = IEx.Config.ansi_docs() do
      IO.ANSI.Docs.print_heading(heading, opts)
      IO.write(types)
      IO.ANSI.Docs.print(doc, opts)
    else
      IO.puts("* #{heading}\n")
      IO.write(types)
      IO.puts(doc)
    end
  end

  defp print_typespec({types, doc}) do
    IO.puts(types)

    if opts = IEx.Config.ansi_docs() do
      doc && IO.ANSI.Docs.print(doc, opts)
    else
      doc && IO.puts(doc)
    end
  end

  defp elixir_module?(module) do
    function_exported?(module, :__info__, 1)
  end

  defp no_beam(module) do
    case Code.ensure_loaded(module) do
      {:module, _} ->
        puts_error(
          "Beam code not available for #{inspect(module)} or debug info is missing, cannot load typespecs"
        )

      {:error, reason} ->
        puts_error("Could not load module #{inspect(module)}, got: #{reason}")
    end
  end

  defp no_docs(module) do
    puts_error("#{inspect(module)} was not compiled with docs")
  end

  defp types_not_found(for), do: not_found(for, "type information")
  defp docs_not_found(for), do: not_found(for, "documentation")

  defp behaviour_found(for) do
    puts_error("""
    No documentation for function #{for} was found, but there is a callback with the same name.
    You can view callback documentations with the b/1 helper.
    """)
  end

  defp not_found(for, type) do
    puts_error("No #{type} for #{for} was found")
  end

  defp puts_error(string) do
    IO.puts(IEx.color(:eval_error, string))
  end
end


Removed empty autogenerated test

Removed empty autogenerated test

defmodule SmppexTest do
  use ExUnit.Case

  test "the truth" do
    assert 1 + 1 == 2
  end
end


Skip deps check when compiling deps

Skip deps check when compiling deps

defmodule Mix.Tasks.Compile do
  use Mix.Task

  @shortdoc "Compile source files"

  @moduledoc """
  A meta task that compile source files. It simply runs the
  compilers registered in your project. At the end of compilation
  it ensures load paths are set.

  ## Configuration

  * `:compilers` - compilers to be run, defaults to:

      [:elixir, :app]

    It can be configured to handle custom compilers, for example:

      [compilers: [:elixir, :mycompiler, :app]]

  ## Command line options

  * `--list`     - List all enabled compilers.
  * `--no-check` - Skip dependencies check before compilation.

  """
  def run(["--list"]) do
    Mix.Task.load_all

    shell   = Mix.shell
    modules = Mix.Task.all_modules

    docs = lc module inlist modules,
              task = Mix.Task.task_name(module),
              match?("compile." <> _, task),
              doc = Mix.Task.shortdoc(module) do
      { task, doc }
    end

    max = Enum.reduce docs, 0, fn({ task, _ }, acc) ->
      max(size(task), acc)
    end

    sorted = Enum.qsort(docs)

    Enum.each sorted, fn({ task, doc }) ->
      shell.info format('mix ~-#{max}s # ~s', [task, doc])
    end

    shell.info "\nEnabled compilers: #{Enum.join get_compilers, ", "}"
  end

  def run(args) do
    ebin = Mix.project[:compile_path] || "ebin"
    remove_ebin ebin

    Mix.Task.run "deps.loadpaths", args

    changed = Enum.reduce get_compilers, false, fn(compiler, acc) ->
      res = Mix.Task.run("compile.#{compiler}", args)
      acc or res != :noop
    end

    if changed, do: File.touch(ebin)
    Mix.Task.run "loadpaths", args
  end

  defp remove_ebin(ebin) do
    :code.del_path(ebin /> File.expand_path /> binary_to_list)
  end

  defp get_compilers do
    Mix.project[:compilers] || if Mix.Project.defined? do
      [:elixir, :app]
    else
      [:elixir]
    end
  end

  defp format(expression, args) do
    :io_lib.format(expression, args) /> iolist_to_binary
  end
end

defmodule Mix.Tasks.Compile do
  use Mix.Task

  @shortdoc "Compile source files"

  @moduledoc """
  A meta task that compile source files. It simply runs the
  compilers registered in your project. At the end of compilation
  it ensures load paths are set.

  ## Configuration

  * `:compilers` - compilers to be run, defaults to:

      [:elixir, :app]

    It can be configured to handle custom compilers, for example:

      [compilers: [:elixir, :mycompiler, :app]]

  ## Command line options

  * `--list`     - List all enabled compilers.
  * `--no-check` - Skip dependencies check before compilation.

  """
  def run(["--list"]) do
    Mix.Task.load_all

    shell   = Mix.shell
    modules = Mix.Task.all_modules

    docs = lc module inlist modules,
              task = Mix.Task.task_name(module),
              match?("compile." <> _, task),
              doc = Mix.Task.shortdoc(module) do
      { task, doc }
    end

    max = Enum.reduce docs, 0, fn({ task, _ }, acc) ->
      max(size(task), acc)
    end

    sorted = Enum.qsort(docs)

    Enum.each sorted, fn({ task, doc }) ->
      shell.info format('mix ~-#{max}s # ~s', [task, doc])
    end

    shell.info "\nEnabled compilers: #{Enum.join get_compilers, ", "}"
  end

  def run(args) do
    ebin = Mix.project[:compile_path] || "ebin"
    remove_ebin ebin

    Mix.Task.run "deps.loadpaths"

    changed = Enum.reduce get_compilers, false, fn(compiler, acc) ->
      res = Mix.Task.run("compile.#{compiler}", args)
      acc or res != :noop
    end

    if changed, do: File.touch(ebin)
    Mix.Task.run "loadpaths"
  end

  defp remove_ebin(ebin) do
    :code.del_path(ebin /> File.expand_path /> binary_to_list)
  end

  defp get_compilers do
    Mix.project[:compilers] || if Mix.Project.defined? do
      [:elixir, :app]
    else
      [:elixir]
    end
  end

  defp format(expression, args) do
    :io_lib.format(expression, args) /> iolist_to_binary
  end
end


Support Unicode messages

Support Unicode messages

defmodule Serum.IOProxy do
  @moduledoc false

  use GenServer

  message_categories = [
    debug: {[:light_black_background, :black], [:light_black]},
    error: {[:red_background, :light_white], [:light_white]},
    gen: {[:light_green], []},
    info: {[:light_black_background, :white], []},
    mkdir: {[:light_cyan], []},
    plugin: {[:light_magenta], []},
    read: {[:light_yellow], []},
    theme: {[:light_magenta], []},
    warn: {[:yellow_background, :black], [:yellow]}
  ]

  @type config :: %{mute_msg: boolean(), mute_err: boolean()}

  @spec start_link(term()) :: GenServer.on_start()
  def start_link(_args) do
    GenServer.start_link(__MODULE__, [], name: __MODULE__)
  end

  @doc "Gets the current configuration of `Serum.IOProxy`."
  @spec config() :: {:ok, config()}
  def config do
    GenServer.call(__MODULE__, :get_config)
  end

  @doc """
  Configures `Serum.IOProxy`.

  ## Options

  - `mute_err` (boolean): Controls whether outputs to `:stderr` must be
    suppressed. Defaults to `false`.
  - `mute_msg` (boolean): Controls whether outputs to `:stdio` must be
    suppressed. Defaults to `false`.
  """
  @spec config(keyword()) :: :ok
  def config(options) when is_list(options) do
    GenServer.call(__MODULE__, {:set_config, options})
  end

  @doc """
  Prints a message to the standard output.

  Available categories are:
  `#{message_categories |> Keyword.keys() |> inspect()}`
  """
  @spec put_msg(atom(), binary()) :: :ok
  def put_msg(category, msg) do
    GenServer.call(__MODULE__, {:put_msg, category, msg})
  end

  @doc """
  Prints a message to the standard error output.

  Available categories are:
  `#{message_categories |> Keyword.keys() |> inspect()}`
  """
  @spec put_err(atom(), binary()) :: :ok
  def put_err(category, msg) do
    GenServer.call(__MODULE__, {:put_err, category, msg})
  end

  @impl GenServer
  def init(_args) do
    {:ok, %{mute_msg: false, mute_err: false}}
  end

  @impl GenServer
  def handle_call(request, from, state)
  def handle_call(:get_config, _, state), do: {:reply, {:ok, state}, state}

  def handle_call({:set_config, conf}, _, state) do
    conf_map = conf |> Map.new() |> Map.take([:mute_msg, :mute_err])

    {:reply, :ok, Map.merge(state, conf_map)}
  end

  def handle_call({:put_msg, category, msg}, _, state) do
    unless(state.mute_msg, do: IO.puts(format_message(category, msg)))

    {:reply, :ok, state}
  end

  def handle_call({:put_err, category, msg}, _, state) do
    unless(state.mute_err, do: IO.puts(:stderr, format_message(category, msg)))

    {:reply, :ok, state}
  end

  max_length =
    message_categories
    |> Enum.map(fn {category, _} -> to_string(category) end)
    |> Enum.map(&String.length/1)
    |> Enum.max()

  @spec format_message(atom(), binary()) :: IO.chardata()
  defp format_message(category, msg)

  Enum.each(message_categories, fn {category, {head_fmt, body_fmt}} ->
    cat_str = category |> to_string() |> String.upcase()

    header =
      [head_fmt, String.pad_leading(cat_str, max_length + 1), ?\s]
      |> IO.ANSI.format(true)
      |> IO.iodata_to_binary()

    body_fmt =
      [body_fmt, "~ts"]
      |> IO.ANSI.format(true)
      |> IO.iodata_to_binary()

    defp format_message(unquote(category), msg) do
      formatted = :io_lib.format(unquote(body_fmt), [format_newlines(msg)])

      [?\r, unquote(header), ?\s, formatted]
    end
  end)

  @spec format_newlines(binary()) :: IO.chardata()
  defp format_newlines(msg) do
    lines = String.split(msg, ~r/\r?\n/)
    indent = String.duplicate(" ", unquote(max_length) + 3)

    Enum.intersperse(lines, ["\n", indent])
  end
end

defmodule Serum.IOProxy do
  @moduledoc false

  use GenServer

  message_categories = [
    debug: {[:light_black_background, :black], [:light_black]},
    error: {[:red_background, :light_white], [:light_white]},
    gen: {[:light_green], []},
    info: {[:light_black_background, :white], []},
    mkdir: {[:light_cyan], []},
    plugin: {[:light_magenta], []},
    read: {[:light_yellow], []},
    theme: {[:light_magenta], []},
    warn: {[:yellow_background, :black], [:yellow]}
  ]

  @type config :: %{mute_msg: boolean(), mute_err: boolean()}

  @spec start_link(term()) :: GenServer.on_start()
  def start_link(_args) do
    GenServer.start_link(__MODULE__, [], name: __MODULE__)
  end

  @doc "Gets the current configuration of `Serum.IOProxy`."
  @spec config() :: {:ok, config()}
  def config do
    GenServer.call(__MODULE__, :get_config)
  end

  @doc """
  Configures `Serum.IOProxy`.

  ## Options

  - `mute_err` (boolean): Controls whether outputs to `:stderr` must be
    suppressed. Defaults to `false`.
  - `mute_msg` (boolean): Controls whether outputs to `:stdio` must be
    suppressed. Defaults to `false`.
  """
  @spec config(keyword()) :: :ok
  def config(options) when is_list(options) do
    GenServer.call(__MODULE__, {:set_config, options})
  end

  @doc """
  Prints a message to the standard output.

  Available categories are:
  `#{message_categories |> Keyword.keys() |> inspect()}`
  """
  @spec put_msg(atom(), binary()) :: :ok
  def put_msg(category, msg) do
    GenServer.call(__MODULE__, {:put_msg, category, msg})
  end

  @doc """
  Prints a message to the standard error output.

  Available categories are:
  `#{message_categories |> Keyword.keys() |> inspect()}`
  """
  @spec put_err(atom(), binary()) :: :ok
  def put_err(category, msg) do
    GenServer.call(__MODULE__, {:put_err, category, msg})
  end

  @impl GenServer
  def init(_args) do
    {:ok, %{mute_msg: false, mute_err: false}}
  end

  @impl GenServer
  def handle_call(request, from, state)
  def handle_call(:get_config, _, state), do: {:reply, {:ok, state}, state}

  def handle_call({:set_config, conf}, _, state) do
    conf_map = conf |> Map.new() |> Map.take([:mute_msg, :mute_err])

    {:reply, :ok, Map.merge(state, conf_map)}
  end

  def handle_call({:put_msg, category, msg}, _, state) do
    unless(state.mute_msg, do: IO.puts(format_message(category, msg)))

    {:reply, :ok, state}
  end

  def handle_call({:put_err, category, msg}, _, state) do
    unless(state.mute_err, do: IO.puts(:stderr, format_message(category, msg)))

    {:reply, :ok, state}
  end

  max_length =
    message_categories
    |> Enum.map(fn {category, _} -> to_string(category) end)
    |> Enum.map(&String.length/1)
    |> Enum.max()

  @spec format_message(atom(), binary()) :: IO.chardata()
  defp format_message(category, msg)

  Enum.each(message_categories, fn {category, {head_fmt, body_fmt}} ->
    cat_str = category |> to_string() |> String.upcase()

    header =
      [head_fmt, String.pad_leading(cat_str, max_length + 1), ?\s]
      |> IO.ANSI.format(true)
      |> IO.iodata_to_binary()

    body_fmt =
      [body_fmt, "~s"]
      |> IO.ANSI.format(true)
      |> IO.iodata_to_binary()

    defp format_message(unquote(category), msg) do
      formatted = :io_lib.format(unquote(body_fmt), [format_newlines(msg)])

      [?\r, unquote(header), ?\s, formatted]
    end
  end)

  @spec format_newlines(binary()) :: IO.chardata()
  defp format_newlines(msg) do
    lines = String.split(msg, ~r/\r?\n/)
    indent = String.duplicate(" ", unquote(max_length) + 3)

    Enum.intersperse(lines, ["\n", indent])
  end
end