lib/storage/adapters/genserver_adapter/query.ex

defmodule Sorcery.Storage.GenserverAdapter.QueryMeta do
  alias Sorcery.Utils.Maps

  @moduledoc """
  Can be used with the entire state.db under :old_db.
  OR if changes are being made, with the p
  """

  defstruct [
    all_table_keys: MapSet.new(),
    all_entities: MapSet.new(), # Format: MapSet.new([{tk, id}])
    old_db: %{},
    new_db: %{}
  ]

  def new(state) do
    %__MODULE__{new_db: state.db}
  end
  def new(src, state) do
    qm = %__MODULE__{}
    Enum.reduce(src.changes_db, qm, fn {tk, table}, acc ->
      acc = Enum.reduce(table, acc, fn {id, partial_entity}, acc ->
        entity = Map.get(state.db[tk], id, %{})
        new_entity = Map.merge(entity, partial_entity)
        acc
        |> Map.update!(:all_entities, fn set -> MapSet.put(set, {tk, id}) end)
        |> Maps.put_in_p([:old_db, tk, id], entity)
        |> Maps.put_in_p([:new_db, tk, id], new_entity)
      end)

      all_table_keys = MapSet.put(acc.all_table_keys, tk)
      Map.put(acc, :all_table_keys, all_table_keys)
    end)
  end

end


defmodule Sorcery.Storage.GenserverAdapter.Query do
  use Norm
  alias Sorcery.Specs.Portals, as: PT
  alias Sorcery.Specs.Primative, as: T
  alias Sorcery.Storage.GenserverAdapter.Specs, as: AdapterT


  @contract solve_portal(PT.portal(), AdapterT.qmeta()) :: T.db()
  @doc """
  Given a portal, return a db of entities satisfying it.
  """
  def solve_portal(portal, qmeta) do
    tk = Map.get(portal, :tk)
    #old = Map.get(qmeta.old_db, tk, %{})
    new = Map.get(qmeta.new_db, tk, %{})
    table = new #Map.merge(old, new)
            |> Enum.reduce(%{}, fn {id, entity}, acc ->
              if portal_watching_entity?(portal, entity) do
                Map.put(acc, id, entity)
              else
                acc
              end
            end)

    if Enum.empty?(table) do
      %{}
    else
      %{tk => table}
    end
  end


  @contract solve_portals(coll_of(PT.portal()), AdapterT.qmeta()) :: T.db()
  @doc """
  Given a list of portals, return a db of entities satisfying it.
  """
  def solve_portals(portals, qmeta) do
    Enum.reduce(portals, %{}, fn portal, acc -> 
      Map.merge(acc, solve_portal(portal, qmeta))
    end)
  end


  @contract affects_portal?(PT.portal(), AdapterT.qmeta()) :: T.bool
  @doc """
  Determine if a portal is observing any of the entities in the qmeta
  """
  def affects_portal?(portal, qmeta) do
    tk = Map.get(portal, :tk)
    old = Map.get(qmeta.old_db, tk, %{})
    new = Map.get(qmeta.new_db, tk, %{})
    db = Map.merge(old, new)
    Enum.any?(db, fn {_, entity} ->
      portal_watching_entity?(portal, entity)
    end)
  end
  

  @contract affects_portals?(coll_of(PT.portal()), AdapterT.qmeta()) :: T.bool
  def affects_portals?(portals, qmeta) do
    Enum.any?(portals, fn portal ->
      affects_portal?(portal, qmeta)
    end)
  end


  @contract affected_pids(coll_of(PT.portal()), AdapterT.qmeta()) :: coll_of(T.pid())
  @doc """
  Given a list of ALL connected portals, return a list of pids, such that at least one of their portals is affected by the qmeta.
  Careful, we're potentially working with huge amounts of data.
  """
  def affected_pids(all_portals, qmeta) do
    Enum.group_by(all_portals, fn %{pid: pid} -> pid end)
    |> Enum.reduce([], fn {pid, portals}, acc ->
      if affects_portals?(portals, qmeta) do
        [pid | acc]
      else
        acc
      end
    end)
  end


  def entity_matches_clause?(entity, {:or, guards}) do
    Enum.any?(guards, fn guard -> entity_matches_clause?(entity, guard) end)
  end
  def entity_matches_clause?(entity, {:and, guards}) do
    Enum.all?(guards, fn guard -> entity_matches_clause?(entity, guard) end)
  end
  def entity_matches_clause?(entity, {:in, attr, set}) do
    Map.get(entity, attr) in set
  end
  def entity_matches_clause?(entity, {fun, attr, v}) do
    cb = Function.capture(Kernel, fun, 2)
    e = Map.get(entity, attr)
    cb.(e, v)
  end

  def portal_watching_entity?(portal, entity) do
    Enum.all?(portal.resolved_guards, fn guard -> entity_matches_clause?(entity, guard) end)
  end


  @doc """
  Given a list of portals, recalculate such that:
    1. Every tuple of {ref, attr} in a guard will become a MapSet of values in the resolve_guard.
    2. Every entity matching the portal will be found, and indexed
  """
  def resolve_portal(portals, state) do
    Enum.map(portals, fn %{guards: guards} = portal ->
      resolved_guards = Enum.map(guards, fn
        {:in, attr, {ref, ref_attr}} ->
          reffed_portal = Enum.find(portals, fn p -> p.id == ref end)
          index = Map.get(reffed_portal.indices, ref_attr)
          {:in, attr, index}

        guard -> guard
      end)

      portal
      |> Map.put(:resolved_guards, resolved_guards)
      |> build_indices(state)

    end)
  end


  defp build_indices(%{tk: tk} = portal, state) do
    # Get all entities matching
    entities = Enum.reduce(state.db[tk], [], fn {_id, entity}, acc ->
      if portal_watching_entity?(portal, entity) do
        [entity | acc]
      else
        acc
      end
    end)

    indices = Enum.reduce(portal.indices, %{}, fn {attr, _}, acc ->
      index = Enum.map(entities, fn e -> Map.get(e, attr) end) |> MapSet.new()
      Map.put(acc, attr, index)
    end)

    Map.put(portal, :indices, indices)
  end


end