defmodule Tracer.Formatter.Base do
  @doc """
  Format trace message to a string.
  """
  def format_trace({:trace_ts, pid, :call, mfa, timestamp}, opts) do
    "#{inspect(pid)} [#{format_time(timestamp)}] call #{call_mfa(mfa, opts)}"
  end

  def format_trace({:trace_ts, pid, :call, mfa, dump, timestamp}, opts) do
    traces = format_dump(dump)
    "#{inspect(pid)} [#{format_time(timestamp)}] call #{call_mfa(mfa, opts)}#{traces}"
  end

  def format_trace({:trace_ts, pid, :return_from, mfa, return, time}, opts) do
    "#{inspect(pid)} [#{format_time_used(time)}] returned " <>
      "#{return_mfa(mfa)}#{inspect(return, opts)}"
  end

  def format_trace({:trace_ts, pid, :exception_from, mfa, {class, value}, time}, _opts) do
    "#{inspect(pid)} [#{format_time_used(time)}] exception " <>
      "#{return_mfa(mfa)}#{inspect(class)}:#{inspect(value)}"
  end

  def format_trace(msg, opts) do
    "unknown message: #{inspect(msg, opts)}"
  end

  defp call_mfa({module, function, arguments}, opts) do
    "#{inspect(module)}.#{function}(" <>
      Enum.map_join(arguments, ", ", &inspect(&1, opts)) <> ")"
  end

  defp return_mfa({module, function, argument}) do
    "#{inspect(module)}.#{function}/#{argument} -> "
  end

  defp fold_over(list, acc \\ [])

  defp fold_over([_last], acc), do: acc

  defp fold_over([one | tail], acc) do
    fold_over(tail, [extract_function(one) | acc])
  end

  defp extract_function(line) do
    case Regex.run(~r"^.+\((.+):(.+)/(\d+).+\)$", line, capture: :all_but_first) do
      [m, f, a_length] ->
        "\n  #{format_module(m)}.#{format_function(f)}/#{a_length}"

      nil ->
        ""
    end
  end

  def format_module(binatom) do
    case unatom(binatom) do
      "Elixir." <> binatom -> binatom
      binatom -> ":#{binatom}"
    end
  end

  defp format_function(binatom) do
    unatom(binatom)
  end

  defp unatom(binatom) do
    body_size = byte_size(binatom) - 2

    case binatom do
      <<"'", body::binary-size(body_size), "'">> -> body
      _ -> binatom
    end
  end

  @doc """
  Format dump to call traces
  """
  def format_dump(dump) do
    String.split(dump, "\n")
    |> Enum.filter(&Regex.match?(~r/Return addr 0x|CP: 0x/, &1))
    |> fold_over
    |> Enum.reverse()
  end

  @doc """
  Format time used
  """
  def format_time_used(time) when time < 1000, do: time
  def format_time_used(time), do: "#{div(time, 1000)}ms"

  @doc """
  Format time
  """
  def format_time({_, _, _} = now), do: now |> to_time() |> format_time()

  def format_time({hh, mi, ss, ms}) do
    [pad2(hh), ?:, pad2(mi), ?:, pad2(ss), ?., pad3(ms)]
  end

  defp to_time({_, _, micro} = now) do
    {_, {hours, minutes, seconds}} = :calendar.now_to_universal_time(now)
    {hours, minutes, seconds, div(micro, 1000)}
  end

  defp pad3(int) when int < 10, do: [?0, ?0, Integer.to_string(int)]
  defp pad3(int) when int < 100, do: [?0, Integer.to_string(int)]
  defp pad3(int), do: Integer.to_string(int)

  defp pad2(int) when int < 10, do: [?0, Integer.to_string(int)]
  defp pad2(int), do: Integer.to_string(int)
end
