lib/statechart.ex

defmodule Statechart do
  [_ignored_intro, usage_section, _ignored] =
    "README.md" |> File.read!() |> String.split(~r/<!---.*moduledoc.*-->/, parts: 3)

  @external_resource "README.md"
  @moduledoc usage_section

  alias Statechart.Build.AccStep
  alias Statechart.Build.MacroState
  alias Statechart.Build.MacroChart
  alias Statechart.Build.MacroOpts
  alias Statechart.Build.MacroTransition
  alias Statechart.Build.MacroSubchart
  alias Statechart.Build.MacroTransition
  alias Statechart.Machine
  alias Statechart.Schema.Location

  @type t(context) :: %Statechart.Machine{
          # LATER i don't like this key name
          statechart_module: atom(),
          context: context,
          current_local_id: local_id(),
          last_event_status: :ok | :error
        }

  @opaque local_id :: Location.local_id()
  @opaque t :: t(term)
  @type event :: term()
  @type state :: atom()
  @type action :: (context() -> context()) | (() -> :ok)
  @type context :: term

  defmacro __using__(_opts) do
    quote do
      import Statechart,
        only: [
          statechart: 0,
          statechart: 1,
          statechart: 2,
          subchart_new: 1,
          subchart_new: 2
        ]

      require MacroChart
      require AccStep
    end
  end

  @doc section: :build
  @doc """
  Create a statechart node.

  Examples

  arity-1 (name only)

      statechart do
        state :my_only_state
      end

  arity-2 (name and opts)

      statechart do
        state :state_with_opts, entry: fn -> IO.inspect "hello!" end
                                exit: fn -> IO.inspect "bye" end
      end

  arity-2 (name and do block)

      statechart do
        state :parent_state do
          state :child_state
        end
      end


  arity-3 (name and opts and do-block)
      statechart do
        state :parent_state,
          entry: fn -> IO.inspect("hello!") end,
          exit: fn -> IO.inspect("bye") end do
          state :child_state
        end
      end

  module's statechart.
  The way to have multiple nodes sharing the same name is to define statechart
  partials in separate module and then insert those partials into a parent statechart.

  #{MacroOpts.docs(:state)}
  """
  @spec state(state(), Keyword.t(), term()) :: term
  defmacro state(name, opts, do_block)
  defmacro state(name, opts, do: block), do: MacroState.build_ast(name, opts, block)

  @doc """
  Create a statechart node.

  See `state/3` for details
  """
  @doc section: :build
  @spec state(state(), Keyword.t() | term()) :: term
  defmacro state(name, opts_or_do_block \\ [])
  defmacro state(name, do: block), do: MacroState.build_ast(name, [], block)
  defmacro state(name, opts), do: MacroState.build_ast(name, opts, nil)

  @doc section: :build
  @doc """
  Create and register a statechart to this module.

  ```
  defmodule ToggleStatechart do
    use Statechart

    statechart do
      state :on, default: true, do: :TOGGLE >>> :off
      state :off, do: :TOGGLE >>> :on
    end
  end
  ```
  #{MacroOpts.docs(:statechart)}
  """

  @doc section: :build
  defmacro statechart(opts, do_block)
  defmacro statechart(opts, do: block), do: MacroChart.build_ast(:statechart, opts, block)

  @doc """
  Create or register a statechart to this module.

  See `statechart/2` for details.
  """
  @doc section: :build
  defmacro statechart(opts_or_do_block \\ [])

  defmacro statechart(do: block), do: MacroChart.build_ast(:statechart, [], block)
  defmacro statechart(opts), do: MacroChart.build_ast(:statechart, opts, nil)

  # FutureFeature
  @doc false
  # Inject a chart defined in another module.
  # ## `StatechartError` raised when...
  # - `subchart/2` is passed anything besides the name of a module that containing a `statechart/2` call
  # - `state/2` is called outside of a `statechart` block
  defmacro subchart(name, module, opts \\ [], do_block \\ [do: nil])

  defmacro subchart(name, module, opts, do: block) do
    MacroSubchart.build_ast(name, module, opts, block)
  end

  # LATER rename to subchart, add doc, and make public
  # have to then remove the current subchart and absorb its functionality into `state`
  _doc = """
  blarg

  blarg

  #{MacroOpts.docs(:subchart)}
  """

  @doc false
  defmacro subchart_new(), do: MacroChart.build_ast(:subchart, [], nil)
  @doc false
  defmacro subchart_new(do: block), do: MacroChart.build_ast(:subchart, [], block)

  defmacro subchart_new(opts) do
    MacroChart.build_ast(:subchart, opts, nil)
  end

  @doc false
  defmacro subchart_new(opts, block), do: MacroChart.build_ast(:subchart, opts, block)
  @doc section: :build
  @doc """
  Register a transtion from an event and target state.
  """
  defmacro event >>> target_state do
    MacroTransition.build_ast(event, target_state)
  end

  @doc section: :manipulate
  @doc """
  Get current context data.
  """
  @spec context(t(context)) :: context when context: var
  defdelegate context(statechart), to: Machine

  @doc false
  defmacro root() do
    quote do: __MODULE__
  end

  @doc section: :manipulate
  @doc """
  Send an event to the statechart
  """
  @spec trigger(t(context), event) :: t(context) when context: var
  defdelegate trigger(statechart, event), to: Machine

  @doc section: :manipulate
  @doc """
  Determine if the given state is in the given compound state
  """
  @spec in_state?(t, state) :: boolean
  defdelegate in_state?(statechart, state), to: Machine

  @doc section: :manipulate
  @doc """
  Returns `:ok` if last event was valid and caused a transition
  """
  @spec last_event_status(t) :: :ok | :error
  defdelegate last_event_status(statechart), to: Machine

  @doc section: :manipulate
  @doc """
  Get the current compound state
  """
  @spec states(t) :: [state]
  defdelegate states(statechart), to: Machine, as: :states
end