lib/orion_web/page_live.ex

defmodule OrionWeb.PageLive do
  use OrionWeb, :live_view

  alias Orion.MatchSpec

  @moduledoc """
  This is the central LiveView for the Orion UI.

  It handles the form to pass a matchspec, store then in the Orion.MatchSpecDB,
  then start the chart as needed by passing the ref to that line to the chart liveview
  in the sessions.

  It also handles pause/start

  TODO: change this, to allow multiple users at once

  """

  @impl true
  def render(assigns) do
    ~H"""
    <header class="m-8 max-2-screen-xl w-11/12 mx-auto text-4xl text-center font-bold">
      <a href="https://github.com/LivewareProblems/Orion" target="_blank"> Orion </a>
    </header>
    <section class="mx-auto m-4 rounded-lg max-w-screen-xl min-w-sm w-11/12" id="trace-form">
      <div class="flex flex-row justify-evenly">
        <.form for={:start_pause} phx-submit="start_pause_submit" class="self-stretch flex flex-row">
          <%= submit(pause_state_text(@pause_state),
            class:
              "self-end w-30 rounded py-2 px-3 mr-3 mb-4 bg-teal-60 text-white hover:bg-dusk-50 hover:text-black"
          ) %>
        </.form>

        <.form
          :let={f}
          for={:match_spec}
          phx-submit="query_submit"
          phx-change="query_validate"
          class="flex flex-row justify-between flex-wrap grow"
        >
          <div class="flex flex-col flex-grow">
            <%= label(f, :module_name, "Module", class: "text-black mx-3 my-2 font-medium") %>
            <%= text_input(f, :module_name,
              value: @form_value.module,
              placeholder: "Module",
              class:
                "border-2 border-gray-60 bg-white text-black hover:border-teal-50 focus:border-blue-50 rounded py-2 px-2 mx-2 mb-3 leading-tight focus:outline-none"
            ) %>
          </div>

          <div class="flex flex-col flex-grow">
            <%= label(f, :function_name, "Function", class: "text-black mx-3 my-2 font-medium") %>
            <%= text_input(f, :function_name,
              value: @form_value.function,
              placeholder: "Function",
              class:
                "border-2 border-gray-60 bg-white text-black hover:border-teal-50 focus:border-blue-50 rounded py-2 px-1 mx-2 mb-3 leading-tight focus:outline-none"
            ) %>
          </div>

          <div class="flex flex-col flex-grow">
            <%= label(f, :arity, "Arity", class: "text-black mx-3 my-2 font-medium") %>
            <%= number_input(f, :arity,
              value: @form_value.arity,
              placeholder: "Arity",
              class:
                "border-2 border-gray-60 bg-white text-black focus:border-blue-50 hover:border-teal-50 rounded py-2 px-1 mx-2 mb-3 leading-tight focus:outline-none"
            ) %>
          </div>

          <div :if={@fake_data} class="flex flex-col flex-grow">
            <%= label(f, :fake_data, "Fake?", class: "text-black mx-2 my-2 font-medium") %>
            <%= checkbox(f, :fake_data,
              value: @form_value.fake,
              class:
                "mt-2 border-2 border-gray-60 bg-white text-black focus:border-blue-50 hover:border-teal-50 rounded py-2 px-1 mx-4 leading-tight focus:outline-none"
            ) %>
          </div>

          <%= submit("Run",
            "phx-disable-with": "Setting up Trace...",
            class:
              "self-end rounded py-3 px-4 mt-3 mb-4 bg-dusk-60 text-white hover:bg-dusk-50 hover:text-black"
          ) %>
        </.form>
      </div>
    </section>

    <section id="chart-list" class="mx-auto m-4 rounded-lg max-w-screen-xl min-w-sm w-11/12">
      <%= for %{name: name, key: key, session: session} <- @chart_list do %>
        <%= live_render(@socket, OrionWeb.MeasurementLive,
          id: "#{name}-#{key}",
          session: %{"key" => session}
        ) %>
      <% end %>
    </section>
    """
  end

  @impl true
  def mount(_params, session, socket) do
    session_id = :crypto.strong_rand_bytes(20) |> Base.encode64()

    socket =
      socket
      |> assign_new(:chart_list, fn -> [] end)
      |> assign_new(:pause_state, fn -> :waiting end)
      |> assign_new(:form_value, fn ->
        %{
          module: "",
          function: "",
          arity: 0,
          fake: "false"
        }
      end)
      |> assign_new(:current_key, fn -> 1 end)
      |> assign_new(:session_id, fn -> session_id end)
      |> assign(:self_profile, Map.get(session, "self_profile", true))
      |> assign(:fake_data, Map.get(session, "fake_data", false))

    {:ok, socket}
  end

  @impl true
  def handle_event("query_validate", %{"match_spec" => query}, socket) do
    socket =
      assign(socket, %{
        form_value: %{
          module: query["module_name"],
          function: query["function_name"],
          arity: query["arity"],
          fake: query["fake_data"]
        }
      })

    {:noreply, socket}
  end

  @impl true
  def handle_event("query_submit", %{"match_spec" => query}, socket) do
    new_match_spec = %MatchSpec{
      module_name: query["module_name"],
      function_name: query["function_name"],
      arity: query["arity"]
    }

    new_pause_state =
      case socket.assigns.pause_state do
        :waiting -> :running
        status -> status
      end

    session =
      Orion.MatchSpecStore.new(socket.assigns.current_key, new_match_spec, %{
        self_profile: socket.assigns.self_profile,
        fake_data: query["fake_data"] == "true",
        start_pause_status: new_pause_state,
        id: socket.assigns.session_id
      })

    data = %{
      match_spec: new_match_spec,
      chart_list: [
        %{
          name:
            "#{new_match_spec.module_name}-#{new_match_spec.function_name}-#{new_match_spec.arity}",
          key: socket.assigns.current_key,
          session: session
        }
        | socket.assigns.chart_list
      ],
      form_value: %{
        module: "",
        function: "",
        arity: 0,
        fake: "false"
      },
      current_key: socket.assigns.current_key + 1,
      pause_state: new_pause_state
    }

    socket = assign(socket, data)

    {:noreply, socket}
  end

  @impl true
  def handle_event("start_pause_submit", _, socket) do
    # we use a Map get because there may be _no_ :pause_state state, as we initialise it lazily.
    socket =
      case socket.assigns.pause_state do
        :paused ->
          Orion.SessionPubsub.dispatch(socket.assigns.session_id, :start)

          assign(socket, :pause_state, :running)

        :running ->
          Orion.SessionPubsub.dispatch(socket.assigns.session_id, :pause)
          assign(socket, :pause_state, :paused)

        _ ->
          socket
      end

    {:noreply, socket}
  end

  @impl true
  def handle_info({:remove, key}, socket) do
    chart_list = Enum.reject(socket.assigns.chart_list, fn map -> map.key == key end)

    {:noreply, assign(socket, :chart_list, chart_list)}
  end

  def pause_state_text(atom) do
    case atom do
      :paused ->
        "Start"

      :running ->
        "Pause"

      _ ->
        "Start/Pause"
    end
  end
end