lib/arke/boundary/unit_manager.ex

# Copyright 2023 Arkemis S.r.l.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
#      http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.

defmodule Arke.Boundary.UnitManager do
  defmacro __using__(_) do
    quote do
      use GenServer
      alias Arke.Core.Unit
      alias Arke.Utils.ErrorGenerator, as: Error
      @compile {:parse_transform, :ms_transform}

      Module.register_attribute(__MODULE__, :manager_id, accumulate: false, persist: true)

      Module.register_attribute(__MODULE__, :arke_list, accumulate: true, persist: true)
      Module.register_attribute(__MODULE__, :group_list, accumulate: true, persist: false)

      import unquote(__MODULE__), only: [manager_id: 1]

      def arke_list(), do: Keyword.get(__MODULE__.__info__(:attributes), :arke_list, [])
      def group_list(), do: Keyword.get(__MODULE__.__info__(:attributes), :arke_list, [])

      def manager_id,
        do: Keyword.get(__MODULE__.__info__(:attributes), :manager_id, []) |> List.first()

      # client
      def start_link(state \\ []) do
        GenServer.start_link(__MODULE__, state, name: __MODULE__)
      end

      # server
      @impl true
      def init(arg) do
        :ets.new(manager_id, [:set, :named_table, :public, read_concurrency: true])
        # do not block the init
        {:ok, arg}
      end

      def get_all(project \\ :arke_system) do
        fun =
          :ets.fun2ms(fn {{unit_id, project_id}, _unit} when project_id == project ->
            {unit_id, project_id}
          end)

        :ets.select(manager_id, fun)
      end

      def get(unit_id, _) when is_nil(unit_id), do: nil

      def get(unit_id, project) when is_binary(unit_id) do
        get(String.to_existing_atom(unit_id), project)
      rescue
        ArgumentError -> nil
      end

      def get(unit_id, project) do
        case :ets.lookup(manager_id, {unit_id, project}) do
          [{_, unit}] ->
            unit

          [] ->
            case :ets.lookup(manager_id, {unit_id, :arke_system}) do
              [{_, unit}] -> unit
              [] -> nil
            end
        end
      end

      def remove(%{id: id, metadata: %{project: project}} = unit), do: remove(id, project)

      def remove(unit_id, project) do
        case get(unit_id, project) do
          nil ->
            {:error, "#{unit_id} doesn't exist in project: #{project}"}

          _ ->
            :ets.delete(manager_id, {unit_id, project})
            :ok
        end
      end

      def create(unit), do: create(unit, [])

      def create(%{id: id, metadata: %{project: project}} = unit, opts) when is_list(opts),
        do: create(unit, project, opts)

      def create(unit, project), do: create(unit, project, [])

      def create(unit, project, opts) do
        {manager, opts} = Keyword.pop(opts, :manager, __MODULE__)
        {unit, project} = before_create(unit, project)
        GenServer.call(manager, {:create, unit, project})
      end

      def before_create(unit, project), do: {unit, project}

      def update(%{id: id, metadata: %{project: project}} = unit, new_unit),
        do: update(id, project, new_unit)

      def update(unit_id, project, new_unit) do
        unit = get(unit_id, project)
        GenServer.call(__MODULE__, {:update, new_unit, project})
      end

      def call_func(%{id: id, metadata: %{project: project}} = unit, func, opts),
        do: call_func(id, project, func, opts)

      def call_func(unit_id, project, func, opts),
        do: get(unit_id, project) |> exec_call_func(func, opts)

      defp exec_call_func(unit, func, opts) when is_nil(unit),
        do: get(:arke, :arke_system) |> exec_call_func(func, opts)

      defp exec_call_func(%{__module__: module} = unit, func, opts) when is_nil(module),
        do: {:error, "No Module"}

      defp exec_call_func(
             %{id: id, metadata: %{project: project}, __module__: module} = unit,
             func,
             opts
           ) do
        try do
          apply(module, func, opts)
        rescue
          e ->
            IO.inspect(e)
            {:error, "Undefined function"}
        end
      end

      ####
      # Link
      ####
      def get_link(%{id: id, metadata: %{project: project}} = unit, parameter_id),
        do: get_link(id, project, parameter_id)

      def get_link(unit_id, project, parameter_id) do
        case get(unit_id, project) do
          nil -> {:error, "#{unit_id} doesn't exist in project: #{project}"}
          %{data: data} = unit -> Enum.map(Map.get(data, parameter_id, []), fn l -> l end)
        end
      end

      def add_link(unit, parameter_id, child_id, metadata)

      def add_link(
            %{id: id, metadata: %{project: project}} = unit,
            parameter_id,
            child_id,
            metadata
          ),
          do: add_link(id, project, parameter_id, child_id, metadata)

      def add_link(unit_id, project, parameter_id, child_id, metadata) do
        manager = __MODULE__

        case get(unit_id, project) do
          nil -> {:error, "#{unit_id} doesn't exist in project: #{project}"}
          unit -> GenServer.call(manager, {:add_link, unit, parameter_id, child_id, metadata})
        end
      end

      def remove_link(unit, parameter_id, child_id)

      def remove_link(%{id: id, metadata: %{project: project}} = unit, parameter_id, child_id),
        do: remove_link(id, project, parameter_id, child_id)

      def remove_link(unit_id, project, parameter_id, child_id) do
        manager = __MODULE__

        case get(unit_id, project) do
          nil -> {:error, "#{unit_id} doesn't exist in project: #{project}"}
          unit -> GenServer.call(manager, {:remove_link, unit, parameter_id, child_id})
        end
      end

      ######
      ## HANDLE
      ######

      # Update Unit
      def handle_call({:create, %{metadata: metadata} = unit, project}, _from, state) do
        unit = Unit.update(unit, metadata: Map.put(metadata, :project, project))
        :ets.insert(manager_id, {{unit.id, project}, unit})
        {:reply, unit, state}
      end

      # Update Unit
      def handle_call({:update, new_unit, project}, _from, state) do
        :ets.insert(manager_id, {{new_unit.id, project}, new_unit})
        {:reply, new_unit, state}
      end

      # Call handle link

      # Add link
      def handle_call(
            {:add_link, %{data: data, metadata: %{project: project}} = unit, parameter_id,
             child_id, metadata},
            _from,
            state
          ) do
        opts =
          %{}
          |> Map.put(parameter_id, [
            link_init(project, parameter_id, child_id, metadata) | Map.get(data, parameter_id, [])
          ])

        unit = Unit.update(unit, opts)
        :ets.insert(manager_id, {{unit.id, project}, unit})

        {:reply, unit, state}
      end

      defp link_init(project, parameter_id, child_id, metadata),
        do: %{id: child_id, metadata: metadata}

      # Remove link
      def handle_call(
            {:remove_link, %{data: data, metadata: %{project: project}} = unit, parameter_id,
             child_id},
            _from,
            state
          ) do
        opts =
          %{}
          |> Map.put(
            parameter_id,
            Enum.filter(Map.get(data, parameter_id, []), fn link -> link.id != child_id end)
          )

        unit = Unit.update(unit, opts)
        :ets.insert(manager_id, {{unit.id, project}, unit})
        {:reply, unit, state}
      end

      def handle_info({:EXIT, _from, reason}, state) do
        Logger.warn("Tracking #{state.name} - Stopped with reason #{inspect(reason)}")
      end

      def terminate(reason, _s) do
        IO.inspect({self(), reason}, label: :terminate)
        :ok
      end

      defoverridable before_create: 2, link_init: 4
    end
  end

  defmacro manager_id(name) do
    quote do
      @manager_id unquote(name)
    end
  end
end