defmodule LiveDebugger.Structs.Trace do
  @moduledoc """
  This module provides a struct to represent a trace.

  ID is a number generated by :dbg tracer.
  PID is always present.
  CID is optional - it is filled when trace comes from LiveComponent.

  When trace has PID and CID it means that it comes from LiveComponent.
  When trace has only PID it means that it comes from LiveView.
  There cannot be a trace with CID and without PID.
  """

  alias LiveDebugger.CommonTypes

  defstruct [
    :id,
    :module,
    :function,
    :arity,
    :args,
    :socket_id,
    :transport_pid,
    :pid,
    :cid,
    :timestamp,
    :execution_time,
    :type
  ]

  @type id() :: neg_integer() | 0

  @type t() :: %__MODULE__{
          id: id(),
          module: module(),
          function: atom(),
          arity: non_neg_integer(),
          args: list(),
          socket_id: String.t(),
          transport_pid: pid() | nil,
          pid: pid(),
          cid: CommonTypes.cid() | nil,
          timestamp: non_neg_integer(),
          execution_time: non_neg_integer() | nil,
          type: :call | :return_from | :exception_from
        }

  @doc """
  Creates a new trace struct.
  """
  @spec new(integer(), module(), atom(), list(), pid(), :erlang.timestamp(), Keyword.t()) :: t()
  def new(id, module, function, args, pid, timestamp, opts \\ []) do
    socket_id = Keyword.get(opts, :socket_id, get_socket_id_from_args(args))
    transport_pid = Keyword.get(opts, :transport_pid, get_transport_pid_from_args(args))
    cid = Keyword.get(opts, :cid, get_cid_from_args(args))
    type = Keyword.get(opts, :type, :call)

    %__MODULE__{
      id: id,
      module: module,
      function: function,
      arity: length(args),
      args: args,
      socket_id: socket_id,
      transport_pid: transport_pid,
      pid: pid,
      cid: cid,
      timestamp: :timer.now_diff(timestamp, {0, 0, 0}),
      execution_time: nil,
      type: type
    }
  end

  @doc """
  Returns the node id from the trace.
  It is PID if trace comes from a LiveView, CID if trace comes from a LiveComponent.
  """
  @spec node_id(t()) :: pid() | CommonTypes.cid()
  def node_id(%__MODULE__{cid: cid}) when not is_nil(cid), do: cid
  def node_id(%__MODULE__{pid: pid}), do: pid

  @doc """
  Checks if the trace is a delete live component trace.
  """
  @spec live_component_delete?(t()) :: boolean()
  def live_component_delete?(%__MODULE__{
        module: Phoenix.LiveView.Diff,
        function: :delete_component,
        arity: 2
      }) do
    true
  end

  def live_component_delete?(_), do: false

  @spec callback_name(t()) :: String.t()
  def callback_name(%__MODULE__{function: function, arity: arity}) do
    "#{function}/#{arity}"
  end

  @spec arg_name(t(), non_neg_integer()) :: String.t()
  def arg_name(trace, arg_index)

  # Callbacks common for LiveView and LiveComponent
  def arg_name(%__MODULE__{function: :handle_async}, 0), do: "name"
  def arg_name(%__MODULE__{function: :handle_async}, 1), do: "async_fun_result"
  def arg_name(%__MODULE__{function: :handle_async}, 2), do: "socket"

  def arg_name(%__MODULE__{function: :handle_call}, 0), do: "message"
  def arg_name(%__MODULE__{function: :handle_call}, 1), do: "from"
  def arg_name(%__MODULE__{function: :handle_call}, 2), do: "socket"

  def arg_name(%__MODULE__{function: :handle_cast}, 0), do: "message"
  def arg_name(%__MODULE__{function: :handle_cast}, 1), do: "socket"

  def arg_name(%__MODULE__{function: :handle_event}, 0), do: "event"
  def arg_name(%__MODULE__{function: :handle_event}, 1), do: "unsigned_params"
  def arg_name(%__MODULE__{function: :handle_event}, 2), do: "socket"

  def arg_name(%__MODULE__{function: :handle_info}, 0), do: "message"
  def arg_name(%__MODULE__{function: :handle_info}, 1), do: "socket"

  def arg_name(%__MODULE__{function: :handle_params}, 0), do: "unsigned_params"
  def arg_name(%__MODULE__{function: :handle_params}, 1), do: "uri"
  def arg_name(%__MODULE__{function: :handle_params}, 2), do: "socket"

  def arg_name(%__MODULE__{function: :render}, 0), do: "assigns"

  def arg_name(%__MODULE__{function: :terminate}, 0), do: "reason"
  def arg_name(%__MODULE__{function: :terminate}, 1), do: "socket"

  # LiveView specific
  def arg_name(%__MODULE__{function: :mount, arity: 3}, 0), do: "params"
  def arg_name(%__MODULE__{function: :mount, arity: 3}, 1), do: "session"
  def arg_name(%__MODULE__{function: :mount, arity: 3}, 2), do: "socket"

  # LiveComponent specific
  def arg_name(%__MODULE__{function: :mount, arity: 1}, 0), do: "socket"

  def arg_name(%__MODULE__{function: :update}, 0), do: "assigns"
  def arg_name(%__MODULE__{function: :update}, 1), do: "socket"

  def arg_name(%__MODULE__{function: :update_many}, 0), do: "list"

  defp get_transport_pid_from_args(args) do
    args
    |> Enum.map(&maybe_get_transport_pid(&1))
    |> Enum.find(fn elem -> is_pid(elem) end)
  end

  defp maybe_get_transport_pid(%{transport_pid: transport}), do: transport
  defp maybe_get_transport_pid(%{socket: %{transport_pid: transport}}), do: transport
  defp maybe_get_transport_pid(_), do: nil

  defp get_socket_id_from_args(args) do
    args
    |> Enum.map(&maybe_get_socket_id(&1))
    |> Enum.find(fn elem -> is_binary(elem) end)
  end

  defp maybe_get_socket_id(%Phoenix.LiveView.Socket{id: id}), do: id
  defp maybe_get_socket_id(%{socket: %Phoenix.LiveView.Socket{id: id}}), do: id
  defp maybe_get_socket_id(_), do: nil

  defp get_cid_from_args(args) do
    args
    |> Enum.map(&maybe_get_cid(&1))
    |> Enum.find(fn elem -> is_struct(elem, Phoenix.LiveComponent.CID) end)
  end

  defp maybe_get_cid(%{myself: cid}), do: cid
  defp maybe_get_cid(%{assigns: %{myself: cid}}), do: cid
  defp maybe_get_cid(_), do: nil
end
