defmodule Playwright.SDK.ChannelOwner do
  @moduledoc false
  @callback init(struct(), map()) :: {atom(), struct()}
  @optional_callbacks init: 2

  defmacro __using__(_) do
    quote do
      use Playwright.SDK.ChannelOwner.Macros
      @behaviour Playwright.SDK.ChannelOwner

      @derive {Jason.Encoder, only: [:guid]}
      @derive {Inspect, only: [:guid] ++ @properties}

      import Playwright.SDK.Extra.Map
      alias Playwright.SDK.Channel

      defstruct @properties ++ [:session, :guid, :initializer, :listeners, :parent, :type]

      @typedoc """
      %#{String.replace_prefix(inspect(__MODULE__), "Elixir.", "")}{}
      """
      @type t() :: %__MODULE__{}

      @doc false
      @spec new(struct(), map()) :: {term(), struct()}
      def new(parent, %{guid: guid, type: type, initializer: initializer} = args) do
        base = %__MODULE__{
          session: parent.session,
          guid: guid,
          initializer: initializer,
          listeners: %{},
          parent: parent,
          type: type
        }

        initializer = deep_snakecase_keys(initializer)
        init(struct(base, initializer), initializer)
      end

      @doc """
      Optional **callback** implementation for `Playwright.SDK.ChannelOwner.init/2`.

      If implemented, the callback will receive:

        1. The newly created "channel owner" struct.
        2. The `:initializer` received from the Playwright browser server.

      The implementation has the option of "patching" the struct as stored in
      the catalog, and/or binding event handlers.

      ## Example

          def init(%{session: session} = owner, _initializer) do
            Channel.bind(session, {:guid, owner.guid}, :close, fn event ->
              Logger.warning("Closing \#{inspect(event.target)}")
            end)

            {:ok, %{owner | version: "1.2.3"}}
          end

      ## Returns

        - `{:ok, struct()}`

      ## Arguments

      | key/name    | type   |            | description |
      | ------------- | ------ | ---------- | ----------- |
      | `owner`       | param  | `struct()` | The newly created channel owner (resource). |
      | `initializer` | param  | `struct()` | The initializer received from with the channel owner instance was derived. |
      """
      @spec init(struct(), map()) :: {atom(), struct()}
      def init(owner, _initializer) do
        {:ok, owner}
      end

      defoverridable(init: 2)

      require Logger

      @doc false
      @spec on_event(struct(), Channel.Event.t()) :: {term(), struct()}
      def on_event(owner, %Channel.Event{} = event) do
        listeners = Map.get(owner.listeners, event.type, [])

        event =
          Enum.reduce(listeners, event, fn callback, acc ->
            case callback.(acc) do
              {:patch, target} ->
                Map.put(acc, :target, target)

              _ok ->
                acc
            end
          end)

        {:ok, event.target}
      end

      defp post!(owner, action, params) do
        case Channel.post(owner.session, {:guid, owner.guid}, action, params) do
          # simple "success": send "self"
          {:ok, %{id: _}} ->
            Channel.find(owner.session, {:guid, owner.guid})
        end
      end

      defp returning(%{session: session} = subject, task) do
        task.()
        Channel.find(session, {:guid, subject.guid})
      end

      defp with_latest(subject, task) do
        Channel.find(subject.session, {:guid, subject.guid}) |> task.()
        Channel.find(subject.session, {:guid, subject.guid})
      end
    end
  end

  @doc false
  def from(params, parent) do
    module(params).new(parent, params)
  end

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

  defp module(%{type: "Playwright"}) do
    Playwright
  end

  defp module(%{type: type}) do
    String.to_existing_atom("Elixir.Playwright.#{type}")
  rescue
    ArgumentError ->
      message = "ChannelOwner of type #{inspect(type)} is not yet defined"
      exit(message)
  end

  # ChannelOwner macros
  # ---------------------------------------------------------------------------

  defmodule Macros do
    @moduledoc false
    alias Playwright.SDK.Channel

    defmacro __using__(_args) do
      Module.register_attribute(__CALLER__.module, :properties, accumulate: true)

      quote do
        import Kernel, except: [@: 1]
        import unquote(__MODULE__), only: [@: 1]
      end
    end

    def do_at(module, arg, options \\ []) do
      Module.put_attribute(module, :properties, arg)
      doc = Keyword.get(options, :doc, false)

      quote do
        @doc unquote(doc)
        @spec unquote(arg)(t()) :: term()
        def unquote(arg)(owner) do
          owner =
            if Map.has_key?(owner, :is_closed) && owner.is_closed do
              owner
            else
              Channel.find(owner.session, {:guid, owner.guid})
            end

          field = Map.get(owner, unquote(arg))

          if is_map(field) && Map.has_key?(field, :guid) do
            Channel.find(owner.session, {:guid, field.guid})
          else
            field
          end
        end
      end
    end

    defmacro @{:property, _meta, [arg]} do
      do_at(__CALLER__.module, arg)
    end

    defmacro @{:property, _meta, [arg, arg2]} do
      {:%{}, _, actual} = arg2
      do_at(__CALLER__.module, arg, actual)
    end

    defmacro @expr do
      quote do
        Kernel.@(unquote(expr))
      end
    end
  end
end
