lib/exshome_web/live/app_page.ex

defmodule ExshomeWeb.Live.AppPage do
  @moduledoc """
  Generic module for app pages.
  """
  alias Exshome.Dependency
  alias Phoenix.LiveView
  alias Phoenix.LiveView.Socket
  import Phoenix.LiveView.Helpers

  @callback action() :: atom()
  @callback app_module() :: atom()
  @callback view_module() :: atom()
  @callback icon() :: String.t()
  @callback path() :: String.t()
  @callback dependencies() :: Keyword.t()

  def on_mount(_, _params, _session, %Socket{} = socket) do
    socket =
      LiveView.attach_hook(
        socket,
        :dependency_hook,
        :handle_info,
        &__MODULE__.handle_info/2
      )

    deps =
      for {dependency, key} <- get_dependencies(socket), into: %{} do
        {key, Dependency.subscribe(dependency)}
      end

    {:cont, LiveView.assign(socket, deps: deps)}
  end

  def handle_info({Dependency, {module, value}}, %Socket{} = socket) do
    key = get_dependencies(socket)[module]

    if key do
      socket = LiveView.update(socket, :deps, &Map.put(&1, key, value))

      {:halt, socket}
    else
      {:cont, socket}
    end
  end

  def handle_info(_event, %Socket{} = socket), do: {:cont, socket}

  def render(%{socket: %Socket{} = socket, deps: deps} = assigns) do
    missing_deps =
      deps
      |> Enum.filter(fn {_key, value} -> value == Dependency.NotReady end)
      |> Enum.map(fn {key, _value} -> key end)

    if Enum.any?(missing_deps) do
      ~H"Missing dependencies: <%= inspect(missing_deps) %>"
    else
      template = template_name(socket)
      view_module(socket).render(template, assigns)
    end
  end

  def validate_module!(%Macro.Env{module: module}, _bytecode) do
    NimbleOptions.validate!(
      module.__config__(),
      dependencies: [type: :keyword_list],
      icon: [type: :string]
    )
  end

  defp get_dependencies(%Socket{view: view}), do: view.dependencies()

  defp view_module(%Socket{view: view}) do
    view.view_module()
  end

  defp template_name(%Socket{view: view}), do: view.path()

  defmacro __using__(config) do
    quote do
      @action __MODULE__
              |> Module.split()
              |> List.last()
              |> Macro.underscore()
              |> String.to_atom()

      @view_module __MODULE__
                   |> Module.split()
                   |> Enum.slice(0..0)
                   |> List.insert_at(-1, ["Web", "View"])
                   |> List.flatten()
                   |> Module.safe_concat()

      @app_module __MODULE__
                  |> Module.split()
                  |> Enum.slice(0..0)
                  |> Module.safe_concat()

      alias ExshomeWeb.Live.AppPage
      alias Phoenix.LiveView
      alias Phoenix.LiveView.Socket

      @afer_compile {AppPage, :validate_module!}
      @behaviour AppPage

      @impl AppPage
      def app_module, do: @app_module

      @impl AppPage
      def action, do: @action

      @impl AppPage
      def view_module, do: @view_module

      @impl AppPage
      def path, do: "#{@action}.html"

      @impl AppPage
      def icon, do: unquote(config[:icon] || "")

      @impl AppPage
      def dependencies, do: unquote(config[:dependencies])

      use ExshomeWeb, :live_view
      on_mount(AppPage)

      def __config__, do: unquote(config)

      @impl LiveView
      defdelegate render(assigns), to: AppPage
    end
  end

  @hook_module Application.compile_env(:exshome, :app_page_hook_module)
  if @hook_module do
    defoverridable(handle_info: 2)

    def handle_info(event, %Socket{} = socket) do
      original_result = super(event, socket)
      @hook_module.handle_info(event, socket, original_result)
    end
  end
end