defmodule Finitomata.Hook do
  @moduledoc false

  alias Finitomata.Mix.Events

  @type ast_meta :: keyword()
  @type ast_tuple :: {atom(), ast_meta(), any()}
  @type t :: %{
          __struct__: __MODULE__,
          env: Macro.Env.t(),
          module: module(),
          kind: :def | :defp,
          fun: atom(),
          arity: arity(),
          args: [ast_tuple()],
          guards: [ast_tuple()],
          body: [{:do, ast_tuple()}]
        }
  defstruct ~w|env module kind fun arity args guards body|a

  defimpl Inspect do
    @moduledoc false

    import Inspect.Algebra

    @spec inspect(Finitomata.Hook.t(), Inspect.Opts.t()) ::
            :doc_line
            | :doc_nil
            | binary
            | {:doc_collapse, pos_integer}
            | {:doc_force, any}
            | {:doc_break | :doc_color | :doc_cons | :doc_fits | :doc_group | :doc_string, any,
               any}
            | {:doc_nest, any, :cursor | :reset | non_neg_integer, :always | :break}
    def inspect(%Finitomata.Hook{module: module, fun: fun, args: args, guards: guards}, opts) do
      case Keyword.get(opts.custom_options, :fancy, true) do
        false ->
          inner = [module: module, fun: fun, args: args, guards: guards]
          concat(["#Finitomata.Hook<", to_doc(inner, opts), ">"])

        _ ->
          args = args |> Macro.to_string() |> String.slice(1..-2//1)

          guards =
            case guards do
              [] -> ""
              guards -> " when " <> (guards |> Macro.to_string() |> String.slice(1..-2//1))
            end

          concat([" ↹‹#{inspect(module)}.#{fun}(", args, ")", guards, "›"])
      end
    end
  end

  def __on_definition__(env, :def, :on_transition, [_, _, _, _] = args, guards, body) do
    if compiler?() do
      Events.put(
        :hooks,
        struct(__MODULE__,
          env: env,
          module: env.module,
          kind: :def,
          fun: :on_transition,
          arity: 4,
          args: args,
          guards: guards,
          body: body
        )
      )
    end
  end

  def __on_definition__(_env, _kind, _fun, _args, _guards, _body), do: :ok

  defmacro __before_compile__(env) do
    if Code.ensure_loaded?(Mix) do
      deps? =
        [depth: 1]
        |> Mix.Project.deps_scms()
        |> Map.take([:finitomata, :siblings])
        |> map_size()
        |> Kernel.>(0)

      if not compiler?() and deps? do
        Mix.shell().info([
          [:bright, :yellow, "warning: ", :reset],
          "unhandled finitomata declaration found in ",
          [:bright, :blue, inspect(env.module), :reset],
          ",\n         but we were unable to analyse the correctness of the FSM.\n         Add ",
          [:bright, :cyan, ":finitomata", :reset],
          " compiler to ",
          [:bright, :cyan, "compilers:", :reset],
          " in your ",
          [:bright, :cyan, "mix.exs", :reset],
          "!\n  #{Path.relative_to_cwd(env.file)}:#{env.line}"
        ])
      end
    end

    quote generated: true,
          location: :keep,
          bind_quoted: [module: env.module, file: env.file, line: env.line] do
      states = @__config__[:states]

      @typedoc """
      Kind of event which might be send to initiate the transition.

      ## FSM representation

      ```#{@__config__[:syntax] |> Module.split() |> List.last() |> Macro.underscore()}
      #{@__config__[:syntax].lint(@__config__[:dsl])}
      ```
      """
      @type state :: unquote(Enum.reduce(states, &{:|, [], [&1, &2]}))

      require Logger

      if :on_transition in @__config__[:impl_for] do
        @impl Finitomata
        def on_transition(current, event, event_payload, state_payload) do
          Logger.debug(
            "[✓ ⇄] with: " <>
              inspect(
                current: current,
                event: event,
                event_payload: event_payload,
                state: state_payload
              )
          )

          Finitomata.Transition.guess_next_state(@__config__[:fsm], current, event, state_payload)
        end
      end

      if :on_enter in @__config__[:impl_for] do
        @impl Finitomata
        def on_enter(entering, state) do
          Logger.debug("[← ↹] " <> inspect(state: state, entering: entering))
        end
      end

      if :on_exit in @__config__[:impl_for] do
        @impl Finitomata
        def on_exit(exiting, state) do
          Logger.debug("[→ ↹] " <> inspect(state: state, exiting: exiting))
        end
      end

      if :on_failure in @__config__[:impl_for] do
        @impl Finitomata
        def on_failure(event, payload, state) do
          Logger.warning("[✗ ↹] " <> inspect(state: state, event: event, event_payload: payload))
        end
      end

      if :on_fork in @__config__[:impl_for] do
        @impl Finitomata
        def on_fork(forking, state) do
          Logger.debug("[🗘 ↹] " <> inspect(state: state, forking: forking))
        end
      end

      if :on_start in @__config__[:impl_for] do
        @impl Finitomata
        def on_start(state) do
          Logger.debug("[◎ ↹] " <> inspect(state: state))
        end
      end

      if :on_terminate in @__config__[:impl_for] do
        @impl Finitomata
        def on_terminate(state) do
          Logger.info("[◉ ↹] " <> inspect(state: state))
        end
      end

      if :on_timer in @__config__[:impl_for] do
        @impl Finitomata
        def on_timer(current_state, state) do
          Logger.debug("[✓ ↹] " <> inspect(current_state: current_state, state: state))
        end
      end

      if is_integer(@__config__[:timer]) and not Module.defines?(module, {:on_timer, 2}) do
        raise CompileError,
          file: Path.relative_to_cwd(file),
          line: line,
          description:
            "when `timer: non_neg_integer()` is given to `use Finitomata` " <>
              "there must be `on_timer/2` callback defined"
      end
    end
  end

  @spec details(t()) :: [
          {:args, [ast_tuple()]} | {:body, [{:do, ast_tuple()}]} | {:guards, [ast_tuple()]}
        ]
  @doc false
  def details(%__MODULE__{args: args, guards: guards, body: body}),
    do: [args: args, guards: guards, body: body]

  @spec compiler? :: boolean()
  defp compiler? do
    Events
    |> Process.whereis()
    |> then(fn
      pid when is_pid(pid) -> Process.alive?(pid)
      _ -> false
    end)
  end
end
