defmodule Lexical.RemoteControl.Api.Proxy do
  @moduledoc """
  A bimodal buffering proxy

  This proxy has two modes. In its default mode, it simply forwards function calls to another module, but when
  buffering is activated, it will buffer requests and returned canned responses.
  When a process calls `start_buffering`, it is monitored, and while it's alive, all messages are buffered. When the
  process that calls `start_buffering` exits, the messages that are buffered then have the potential to be emitted.

   Buffered request are subject to the proxy's internal logic. Some requests that are time sensitive
  (like formatting) are dropped. Others are deduplicated, while others are reordered.

  The logic follows below
    `broadcast` - Buffered - Though, those related to other events, like compilation are subject to
                  the rules that govern their source events. Progress messages are sent regardless of
                  buffering.
    `schedule_compile` - Buffered - Only one call is kept
    `compile_document` - Buffered, though only one call per URI is kept, and if a `schedule_compile` call
                         was buffered, all `compile_document` calls are dropped
    `reindex`  - Buffered, though only one call is kept and it is the last thing run.
    `index_running?` - Dropped because it requires an immediate response
    `format`  - Dropped, as it requires an immediate response. Responds immediately with empty changes

  Internally, there are three states: proxying, draining and buffering.
  The proxy starts in proxying mode. Then, when start_buffering is called, it changes to draining mode. This
  mode checks if there are any in-flight calls. If there aren't any, it changes immediately to buffring mode.
  If there are in-flight reqeusts, it waits for them to finish, and then switches to buffer mode. Once in buffer
  mode, requests are buffered until the process that called `start_buffering` exits. When that happens, then
  the requests are de-duplicated and run, and then the proxy returns to proxying mode.

  """

  alias Lexical.Document
  alias Lexical.Document.Changes
  alias Lexical.RemoteControl
  alias Lexical.RemoteControl.Api.Messages
  alias Lexical.RemoteControl.Api.Proxy.BufferingState
  alias Lexical.RemoteControl.Api.Proxy.DrainingState
  alias Lexical.RemoteControl.Api.Proxy.ProxyingState
  alias Lexical.RemoteControl.Api.Proxy.Records
  alias Lexical.RemoteControl.CodeMod
  alias Lexical.RemoteControl.Commands

  import Messages
  import Record
  import Records, only: :macros

  @behaviour :gen_statem

  defrecord :buffer, contents: nil, return: :ok
  defrecord :drop, contents: nil, return: :ok

  # public API

  def start_buffering do
    start_buffering(self())
  end

  def start_buffering(caller) when is_pid(caller) do
    :gen_statem.call(__MODULE__, {:start_buffering, caller})
  end

  # proxied functions

  def broadcast(percent_progress() = message) do
    RemoteControl.Dispatch.broadcast(message)
  end

  def broadcast(message) do
    mfa = to_mfa(RemoteControl.Dispatch.broadcast(message))
    :gen_statem.call(__MODULE__, buffer(contents: mfa))
  end

  def schedule_compile(force? \\ false) do
    project = RemoteControl.get_project()

    mfa = to_mfa(RemoteControl.Build.schedule_compile(project, force?))
    :gen_statem.call(__MODULE__, buffer(contents: mfa))
  end

  def compile_document(document) do
    project = RemoteControl.get_project()

    mfa = to_mfa(RemoteControl.Build.compile_document(project, document))

    :gen_statem.call(__MODULE__, buffer(contents: mfa))
  end

  def reindex do
    mfa = to_mfa(Commands.Reindex.perform())
    :gen_statem.call(__MODULE__, buffer(contents: mfa))
  end

  def index_running? do
    mfa = to_mfa(Commands.Reindex.running?())
    :gen_statem.call(__MODULE__, drop(contents: mfa, return: false))
  end

  def format(%Document{} = document) do
    mfa = to_mfa(CodeMod.Format.edits(document))
    drop = drop(contents: mfa, return: {:ok, Changes.new(document, [])})
    :gen_statem.call(__MODULE__, drop)
  end

  # utility functions

  def buffering? do
    :gen_statem.call(__MODULE__, :buffering?)
  end

  # :gen_statem callbacks
  def start_link do
    :gen_statem.start_link({:local, __MODULE__}, __MODULE__, [], [])
  end

  @impl :gen_statem
  def init(_) do
    {:ok, :proxying, ProxyingState.new()}
  end

  def child_spec(_) do
    %{
      id: __MODULE__,
      start: {__MODULE__, :start_link, []}
    }
  end

  @impl :gen_statem
  def callback_mode, do: :state_functions

  # callbacks for proxying mode

  def proxying({:call, from}, {:start_buffering, caller}, %ProxyingState{} = state) do
    Process.monitor(caller)
    buffering_state = BufferingState.new(caller)

    if ProxyingState.empty?(state) do
      {:next_state, :buffering, buffering_state, {:reply, from, :ok}}
    else
      draining_state = DrainingState.new(buffering_state, state)
      {:next_state, :draining, draining_state, {:reply, from, :ok}}
    end
  end

  def proxying({:call, from}, buffer(contents: contents), %ProxyingState{} = state) do
    state = ProxyingState.apply_mfa(state, from, contents)

    {:keep_state, state}
  end

  def proxying({:call, from}, drop(contents: contents), state) do
    state = ProxyingState.apply_mfa(state, from, contents)
    {:keep_state, state}
  end

  def proxying({:call, from}, :buffering?, state) do
    {:keep_state, state, {:reply, from, false}}
  end

  def proxying(:info, {ref, reply}, %ProxyingState{} = state) when is_reference(ref) do
    ProxyingState.reply(state, ref, reply)
    {:keep_state, state}
  end

  def proxying(:info, {:DOWN, ref, _, _, _}, %ProxyingState{} = state) do
    # Handle the DOWN from the task
    new_state = ProxyingState.consume_reply(state, ref)
    {:keep_state, new_state}
  end

  # Callbacks for the draining mode

  def draining(:info, {ref, reply}, %DrainingState{} = state) when is_reference(ref) do
    DrainingState.reply(state, ref, reply)

    {:keep_state, state}
  end

  def draining({:call, from}, {:start_buffering, _}, %DrainingState{} = state) do
    initiator_pid = state.buffering_state.initiator_pid
    {:keep_state, state, {:reply, from, {:error, {:already_buffering, initiator_pid}}}}
  end

  def draining(
        {:call, from},
        buffer(contents: mfa() = mfa, return: return),
        %DrainingState{} = state
      ) do
    state = DrainingState.add_mfa(state, mfa)
    {:keep_state, state, {:reply, from, return}}
  end

  def draining({:call, from}, drop(return: return), %DrainingState{} = state) do
    {:keep_state, state, {:reply, from, return}}
  end

  def draining({:call, from}, :buffering?, state) do
    {:keep_state, state, {:reply, from, true}}
  end

  def draining(:info, {:DOWN, ref, _, _, _}, %DrainingState{} = state) do
    new_state = DrainingState.consume_reply(state, ref)

    if DrainingState.drained?(new_state) do
      {:next_state, :buffering, state.buffering_state}
    else
      {:keep_state, state}
    end
  end

  # Callbacks for buffering mode

  def buffering({:call, from}, {:start_buffering, _}, %BufferingState{} = state) do
    {:keep_state, state, {:reply, from, {:error, {:already_buffering, state.initiator_pid}}}}
  end

  def buffering(
        {:call, from},
        buffer(contents: mfa() = mfa, return: return),
        %BufferingState{} = state
      ) do
    state = BufferingState.add_mfa(state, mfa)
    {:keep_state, state, {:reply, from, return}}
  end

  def buffering({:call, from}, drop(return: return), %BufferingState{} = state) do
    {:keep_state, state, {:reply, from, return}}
  end

  def buffering(:info, {:DOWN, _, :process, pid, _}, %BufferingState{initiator_pid: pid} = state) do
    state
    |> BufferingState.flush()
    |> Enum.each(&apply/1)

    {:next_state, :proxying, ProxyingState.new()}
  end

  def buffering({:call, from}, :buffering?, state) do
    {:keep_state, state, {:reply, from, true}}
  end

  # Private

  defp apply(mfa(module: module, function: function, arguments: arguments)) do
    apply(module, function, arguments)
  end
end
