lib/wongi/engine/token.ex

# credo:disable-for-this-file Credo.Check.Refactor.Nesting
defmodule Wongi.Engine.Token do
  @moduledoc """
  An intermediate data structure representing a partial match.
  """
  alias Wongi.Engine.Beta
  alias Wongi.Engine.WME

  @type t() :: %__MODULE__{}

  @derive Inspect
  defstruct [:ref, :node_ref, :parents, :wme, :assignments]

  @spec new(any(), list(t()), WME.t() | nil, map()) :: t()
  @doc false
  def new(node, parents, wme, assignments \\ %{}) do
    %__MODULE__{
      ref: make_ref(),
      node_ref: Beta.ref(node),
      parents: MapSet.new(parents),
      wme: wme,
      assignments: assignments
    }
  end

  @doc "Returns the value of a bound variable."
  @spec fetch(t(), atom()) :: {:ok, any()} | :error
  def fetch(%__MODULE__{assignments: assignments, parents: parents}, var) do
    case Map.fetch(assignments, var) do
      {:ok, _value} = ok ->
        ok

      :error ->
        Enum.reduce_while(parents, :error, fn parent, :error ->
          case fetch(parent, var) do
            :error -> {:cont, :error}
            value -> {:halt, value}
          end
        end)
    end
  end

  @doc false
  def fetch(
        %__MODULE__{} = token,
        var,
        extra_assignments
      ) do
    case Map.fetch(extra_assignments, var) do
      {:ok, value} ->
        {:ok, value}

      :error ->
        fetch(token, var)
    end
  end

  @doc false
  def has_wme?(%__MODULE__{wme: wme}, wme), do: true
  def has_wme?(_, _), do: false

  @doc false
  def ancestral_wme?(%__MODULE__{wme: token_wme, parents: parents}, wme) do
    token_wme == wme || Enum.any?(parents, &ancestral_wme?(&1, wme))
  end

  @doc false
  def child_of?(%__MODULE__{parents: parents}, parent),
    do: MapSet.member?(parents, parent)

  @doc false
  def child_of_any?(%__MODULE__{} = token, parents),
    do: Enum.any?(parents, &child_of?(token, &1))

  @doc false
  # TODO: sort out token ownership to make this more straighforward; it is a bit
  # confusing that the token's owner node is one level deeper than you
  # intuitively think, and it makes NCC owner tracking difficult to reason about
  def lineage_of?(%__MODULE__{parents: parents} = token, other) do
    duplicate?(token, other) || Enum.any?(parents, &lineage_of?(&1, other))
  end

  defp duplicate?(token, other) do
    token.wme == other.wme && token.assignments == other.assignments &&
      parent_refs(token) == parent_refs(other)
  end

  defp parent_refs(%__MODULE__{parents: parents}) do
    Enum.map(parents, & &1.ref)
  end
end