lib/dsl.ex

defmodule ALF.DSL do
  alias ALF.Components.{
    Basic,
    Stage,
    Switch,
    DeadEnd,
    GotoPoint,
    Goto,
    Done,
    Plug,
    Unplug,
    Composer,
    Tbd
  }

  alias ALF.DSLError

  defmacro stage(atom, options \\ [opts: []]) do
    count = options[:count] || 1
    opts = options[:opts]

    quote do
      Stage.validate_options(unquote(atom), unquote(options))
      Basic.build_component(Stage, unquote(atom), unquote(count), unquote(opts), __MODULE__)
    end
  end

  defmacro switch(atom, options \\ [opts: []]) do
    opts = options[:opts] || []
    count = options[:count] || 1

    quote do
      Switch.validate_options(unquote(atom), unquote(options))
      branches = build_branches(unquote(options)[:branches])

      switch =
        Basic.build_component(Switch, unquote(atom), unquote(count), unquote(opts), __MODULE__)

      %{switch | branches: branches}
    end
  end

  defmacro goto(name, options \\ [opts: []]) do
    to = options[:to]
    opts = options[:opts] || []
    count = options[:count] || 1

    quote do
      Goto.validate_options(unquote(name), unquote(options))
      goto = Basic.build_component(Goto, unquote(name), unquote(count), unquote(opts), __MODULE__)
      %{goto | to: unquote(to)}
    end
  end

  defmacro goto_point(name, options \\ [count: 1]) do
    count = options[:count] || 1

    quote do
      Basic.build_component(GotoPoint, unquote(name), unquote(count), %{}, __MODULE__)
    end
  end

  defmacro dead_end(name, options \\ [count: 1]) do
    count = options[:count] || 1

    quote do
      Basic.build_component(DeadEnd, unquote(name), unquote(count), %{}, __MODULE__)
    end
  end

  defmacro done(name, options \\ [opts: []]) do
    count = options[:count] || 1
    opts = options[:opts] || []

    quote do
      Done.validate_options(unquote(name), unquote(options))

      Basic.build_component(Done, unquote(name), unquote(count), unquote(opts), __MODULE__)
    end
  end

  defmacro composer(name, options \\ [opts: []]) do
    opts = options[:opts] || []
    memo = options[:memo]
    count = options[:count] || 1

    quote do
      Composer.validate_options(unquote(name), unquote(options))

      composer =
        Basic.build_component(Composer, unquote(name), unquote(count), unquote(opts), __MODULE__)

      %{composer | memo: unquote(memo)}
    end
  end

  defmacro from(module, options \\ [opts: []]) do
    count = options[:count] || 1
    opts = options[:opts] || []

    quote do
      validate_stages_from_options(unquote(options))

      set_options(unquote(module).alf_components, unquote(opts), unquote(count))
    end
  end

  defmacro plug_with(module, options \\ [count: 1], do: block) do
    count = options[:count] || 1

    quote do
      validate_plug_with_options(unquote(options))
      plug = Basic.build_component(Plug, unquote(module), unquote(count), %{}, __MODULE__)

      unplug = Basic.build_component(Unplug, unquote(module), unquote(count), %{}, __MODULE__)

      [plug] ++ unquote(block) ++ [unplug]
    end
  end

  defmacro tbd() do
    quote do
      Basic.build_component(Tbd, unquote(:tbd), 1, %{}, __MODULE__)
    end
  end

  defmacro tbd(name, opts \\ []) do
    count = opts[:count] || 1

    quote do
      Tbd.validate_name(unquote(name))
      tbd = Basic.build_component(Tbd, unquote(name), unquote(count), %{}, __MODULE__)
      %{tbd | count: unquote(count)}
    end
  end

  def validate_stages_from_options(options) do
    dsl_options = [:count, :opts]
    wrong_options = Keyword.keys(options) -- dsl_options

    if Enum.any?(wrong_options) do
      raise DSLError,
            "Wrong options are given for the 'from' macro: #{inspect(wrong_options)}. " <>
              "Available options are #{inspect(dsl_options)}"
    end
  end

  def validate_plug_with_options(options) do
    dsl_options = [:module, :name, :opts, :count]
    wrong_options = Keyword.keys(options) -- dsl_options

    if Enum.any?(wrong_options) do
      raise DSLError,
            "Wrong options are given for the plug_with macro: #{inspect(wrong_options)}. " <>
              "Available options are #{inspect(dsl_options)}"
    end
  end

  def build_branches(branches) do
    branches
    |> Enum.reduce(%{}, fn {key, stages}, final_specs ->
      Map.put(final_specs, key, stages)
    end)
  end

  def set_options(components, opts, count) do
    Enum.map(components, &%{&1 | opts: merge_opts(&1.opts, opts), count: count})
  end

  defp merge_opts(opts, new_opts) do
    opts = if is_map(opts), do: Map.to_list(opts), else: opts
    new_opts = if is_map(new_opts), do: Map.to_list(new_opts), else: new_opts
    Keyword.merge(opts, new_opts)
  end

  defmacro __using__(_opts) do
    quote do
      import ALF.DSL

      @before_compile ALF.DSL

      @spec start() :: :ok
      def start() do
        ALF.Manager.start(__MODULE__, [])
      end

      @spec start(list) :: :ok
      def start(opts) when is_list(opts) do
        ALF.Manager.start(__MODULE__, opts)
      end

      @spec started?() :: true | false
      def started?() do
        ALF.Manager.started?(__MODULE__)
      end

      @spec stop() :: :ok | {:exit, {atom, any}}
      def stop do
        ALF.Manager.stop(__MODULE__)
      end

      @spec call(any, Keyword.t()) :: any | [any] | nil
      def call(event, opts \\ [debug: false]) do
        ALF.Manager.call(event, __MODULE__, opts)
      end

      @spec call(any, Keyword.t()) :: reference
      def cast(event, opts \\ [debug: false, send_result: false]) do
        ALF.Manager.cast(event, __MODULE__, opts)
      end

      @spec stream(Enumerable.t(), Keyword.t()) :: Enumerable.t()
      def stream(stream, opts \\ [debug: false]) do
        ALF.Manager.stream(stream, __MODULE__, opts)
      end

      @spec flow(map(), list(), Keyword.t()) :: Enumerable.t()
      def flow(flow, names, opts \\ [debug: false])

      def flow(flow, names, opts) when is_map(flow) and is_list(names) do
        Enum.reduce(flow, %{}, fn {name, stream}, acc ->
          if name in names do
            stream = ALF.Manager.stream(stream, __MODULE__, opts)
            Map.put(acc, name, stream)
          else
            Map.put(acc, name, stream)
          end
        end)
      end

      def flow(flow, name, opts) when is_map(flow) do
        flow(flow, [name], opts)
      end

      @spec components() :: list(map())
      def components() do
        ALF.Manager.components(__MODULE__)
      end
    end
  end

  defmacro __before_compile__(_env) do
    quote do
      def alf_components, do: List.flatten(@components)
    end
  end
end