defmodule Finitomata.Accessible do
  @moduledoc since: "0.26.0"
  @moduledoc """
  The convenience module, exposing `start_link/1` to embed `Finitomata.Supervisor`
  implementation into a supervision tree in an agnostic way.

  This module implements `Access` in a contrived way, allowing to deal with underlying
  _FSM_ instances in a following way.

  ```elixir
  iex|🌢|1 ▶ Finitomata.Accessible.start_link(type: Finitomata, implementation: L, id: L1)
  {:ok, #PID<0.214.0>}

  iex|🌢|2 ▶ sup = Finitomata.Accessible.lookup(L1)
  %Finitomata.Accessible{
    type: Finitomata,
    id: L1,
    implementation: Finitomata.Test.Log,
    last_event: nil,
    cached_pid: #PID<0.214.0>
  }

  iex|🌢|3 ▶ put_in(sup, ["MyFSM"], :accept)
  07:16:34.736 [debug] [→ ↹] […]
  07:16:34.738 [debug] [✓ ⇄] with: [current: :*, event: :__start__, event_payload: %{payload: nil, __retries__: 1}, state: %{}]
  07:16:34.738 [debug] [← ↹] […]
  07:16:34.738 [debug] [→ ↹] […]
  07:16:34.739 [debug] [✓ ⇄] with: [current: :idle, event: :accept, event_payload: nil, state: %{}]
  07:16:34.739 [debug] [← ↹] […]

  %Finitomata.Accessible{
    type: Finitomata,
    id: L1,
    implementation: Finitomata.Test.Log,
    last_event: {"MyFSM", :accept},
    cached_pid: #PID<0.214.0>
  }

  iex|🌢|4 ▶ get_in(sup, ["MyFSM"])
  #Finitomata<[
    name: {Finitomata.L1.Registry, "MyFSM"},
    state: [current: :accepted, previous: :idle, payload: %{}],
    internals: [errored?: false, persisted?: false, timer: false]
  ]>
  ```
  """

  schema = [
    type: [
      required: false,
      default: Infinitomata,
      type: {:custom, Finitomata, :behaviour, [Finitomata.Supervisor]},
      doc:
        "The actual `Finitomata.Supervisor` implementation (typically, `Finitomata` or `Infinitomata`)"
    ],
    implementation: [
      required: true,
      type: :atom,
      doc: "The actual implementation of _FSM_ (the module, having `use Finitomata` clause)"
    ],
    id: [
      required: false,
      default: nil,
      type: :any,
      doc:
        "The unique `ID` of this _Finitomata_ “branch,” when `nil` the `implementation` value would be used"
    ]
  ]

  @schema NimbleOptions.new!(schema)

  @doc """
  Supervision tree embedder.

  ## Options to `Finitomata.Accessible.start_link/1`

  #{NimbleOptions.docs(@schema)}
  """
  def start_link(opts \\ []) do
    opts = NimbleOptions.validate!(opts, @schema)
    type = Keyword.fetch!(opts, :type)
    implementation = Keyword.fetch!(opts, :implementation)
    id = Keyword.fetch!(opts, :id)

    on_start = type.start_link(id)

    case on_start do
      {:ok, pid} ->
        :persistent_term.put(
          {__MODULE__, id},
          struct!(__MODULE__, type: type, id: id, implementation: implementation, cached_pid: pid)
        )

      {:error, {:already_started, pid}} ->
        :persistent_term.put(
          {__MODULE__, id},
          struct!(__MODULE__, type: type, id: id, implementation: implementation, cached_pid: pid)
        )

      _ ->
        nil
    end

    on_start
  end

  @doc """
  Looks up the `Finitomata.Accessible` instance for the given `id`
  """
  @spec lookup(Finitomata.id()) :: t()
  def lookup(id), do: :persistent_term.get({__MODULE__, id}, nil)

  @typedoc "The convenience struct representing the `Finitomata.Supervisor` with `Access`"
  @type t :: %{
          __struct__: __MODULE__,
          type: Finitomata.Supervisor.t(),
          id: Finitomata.id(),
          implementation: module(),
          last_event: nil | {Finitomata.fsm_name(), Finitomata.event_payload()},
          cached_pid: nil | pid()
        }

  defstruct [:type, :id, :implementation, :last_event, :cached_pid]

  @behaviour Access

  @doc false
  @impl Access
  def fetch(%{__struct__: __MODULE__, type: type, id: id}, fsm_name) do
    case type.state(id, fsm_name, :full) do
      %Finitomata.State{} = state -> {:ok, state}
      _ -> :error
    end
  end

  @doc false
  @impl Access
  def pop(%{__struct__: __MODULE__} = data, _key),
    do: {nil, data}

  @doc false
  @impl Access
  def get_and_update(%{__struct__: __MODULE__, type: type, id: id} = data, fsm_name, function) do
    _ =
      if not type.alive?(id, fsm_name) do
        {:ok, _pid} = type.start_fsm(id, fsm_name, data.implementation, %{})
      end

    state = type.state(id, fsm_name, :full)

    case function.(state) do
      :pop ->
        {state, data}

      {_state, event_payload} ->
        :ok = type.transition(id, fsm_name, event_payload)
        {state, %{data | last_event: {fsm_name, event_payload}}}
    end
  end
end
