lib/playwright/channel/catalog.ex

defmodule Playwright.Channel.Catalog do
  @moduledoc """
  Provides storage and management of ChannelOwner instances.

  `Catalog` implements `GenServer` to maintain state, while domain logic is
  expected to be handled within caller modules such as `Playwright.Channel`.
  """
  use GenServer
  import Playwright.Helpers.ErrorHandling
  alias Playwright.Channel.Error

  defstruct [:awaiting, :storage]

  # module init
  # ---------------------------------------------------------------------------

  @doc """
  Starts a `Playwright.Channel.Catalog` linked to the current process with the
  given "root" resource.

  ## Return Values

  If the `Catalog` is successfully created and initialized, the function
  returns `{:ok, pid}`, where `pid` is the PID of the running `Catalog` server.

  ## Arguments

  | key/name | type   |         | description |
  | -------- | ------ | ------- | ----------- |
  | `root`   | param  | `map()` | The root resource for items in the `Catalog`. Provides the `Session` for its descendants |
  """
  @spec start_link(map()) :: {:ok, pid()}
  def start_link(root) do
    GenServer.start_link(__MODULE__, root)
  end

  # @impl init
  # ---------------------------------------------------------------------------

  @impl GenServer
  def init(root) do
    {:ok,
     %__MODULE__{
       awaiting: %{},
       storage: %{"Root" => root}
     }}
  end

  # module API
  # ---------------------------------------------------------------------------

  @doc """
  Retrieves a resource from the `Catalog` by its `param: guid`.

  If the resource is already present in the `Catalog` that resource is returned
  directly. The desired resource might not yet be in the `Catalog`, in which
  case the request will be considered as "awaiting". An awaiting request will
  later receive a response, when the `Catalog` entry is made, or will time out.

  ## Returns

  - `resource`
  - `{:error, error}`

  ## Arguments

  | key/name   | type   |            | description |
  | ---------- | ------ | ---------- | ----------- |
  | `catalog`  | param  | `pid()`    | PID for the Catalog server |
  | `guid`     | param  | `binary()` | GUID to look up |
  | `:timeout` | option | `float()`  | Maximum time to wait, in milliseconds. Defaults to `30_000` (30 seconds). |
  """
  @spec get(pid(), binary(), map()) :: struct() | {:error, Error.t()}
  def get(catalog, guid, options \\ %{}) do
    with_timeout(options, fn timeout ->
      GenServer.call(catalog, {:get, {:guid, guid}}, timeout)
    end)
  end

  @doc """
  Returns a `List` of resources matching the provided "filter".

  ## Returns

  - [`resource`]
  - []

  ## Arguments

  | key/name  | type   |         | description |
  | --------- | ------ | ------- | ----------- |
  | `catalog` | param  | `pid()` | PID for the Catalog server |
  | `filter`  | param  | `map()` | Attributes for filtering |
  """
  @spec list(pid(), map()) :: [struct()]
  def list(catalog, filter) do
    GenServer.call(catalog, {:list, filter})
  end

  @doc """
  Adds a resource to the `Catalog`, keyed on `:guid`.

  ## Returns

  - `resource` (the same as provided)

  ## Arguments

  | key/name   | type   |            | description |
  | ---------- | ------ | ---------- | ----------- |
  | `catalog`  | param  | `pid()`    | PID for the Catalog server |
  | `resource` | param  | `struct()` | The resource to store |
  """
  @spec put(pid(), struct()) :: struct()
  def put(catalog, %{guid: guid} = resource) do
    GenServer.call(catalog, {:put, {:guid, guid}, resource})
  end

  @doc """
  Removes a resource from the `Catalog`, along with its legacy.

  ## Returns

  - `:ok`

  ## Arguments

  | key/name   | type   |            | description |
  | ---------- | ------ | ---------- | ----------- |
  | `catalog`  | param  | `pid()`    | PID for the Catalog server |
  | `guid`     | param  | `binary()` | GUID for the "parent" |
  """
  @spec rm_r(pid(), binary()) :: :ok
  def rm_r(catalog, guid) do
    children = list(catalog, %{parent: get(catalog, guid)})
    children |> Enum.each(fn child -> rm_r(catalog, child.guid) end)

    rm(catalog, guid)
  end

  # @impl callbacks
  # ---------------------------------------------------------------------------

  @impl GenServer
  def handle_call({:get, {:guid, guid}}, from, %{awaiting: awaiting, storage: storage} = state) do
    item = storage[guid]

    if item do
      {:reply, item, state}
    else
      {:noreply, %{state | awaiting: Map.put(awaiting, guid, from)}}
    end
  end

  @impl GenServer
  def handle_call({:list, filter}, _, %{storage: storage} = state) do
    case filter(Map.values(storage), filter, []) do
      [] ->
        {:reply, [], state}

      result ->
        {:reply, result, state}
    end
  end

  @impl GenServer
  def handle_call({:put, {:guid, guid}, item}, _, %{awaiting: awaiting, storage: storage} = state) do
    {caller, awaiting} = Map.pop(awaiting, guid)
    storage = Map.put(storage, guid, item)

    if caller do
      GenServer.reply(caller, item)
    end

    {:reply, item, %{state | awaiting: awaiting, storage: storage}}
  end

  @impl GenServer
  def handle_call({:rm, guid}, _, %{storage: storage} = state) do
    updated = Map.delete(storage, guid)
    {:reply, :ok, %{state | storage: updated}}
  end

  # private
  # ---------------------------------------------------------------------------

  defp filter([], _attrs, result) do
    result
  end

  defp filter([head | tail], attrs, result) when head.type == "" do
    filter(tail, attrs, result)
  end

  defp filter([head | tail], %{parent: parent, type: type} = attrs, result)
       when head.parent.guid == parent.guid and head.type == type do
    filter(tail, attrs, result ++ [head])
  end

  defp filter([head | tail], %{parent: parent, type: type} = attrs, result)
       when head.parent.guid != parent.guid or head.type != type do
    filter(tail, attrs, result)
  end

  defp filter([head | tail], %{parent: parent} = attrs, result)
       when head.parent.guid == parent.guid do
    filter(tail, attrs, result ++ [head])
  end

  defp filter([head | tail], %{type: type} = attrs, result)
       when head.type == type do
    filter(tail, attrs, result ++ [head])
  end

  defp filter([head | tail], %{guid: guid} = attrs, result)
       when head.guid == guid do
    filter(tail, attrs, result ++ [head])
  end

  defp filter([_head | tail], attrs, result) do
    filter(tail, attrs, result)
  end

  defp rm(catalog, guid) do
    GenServer.call(catalog, {:rm, guid})
  end
end