defmodule Tracker do
  @moduledoc false
  alias __MODULE__

  defstruct total: 0,
            koans: MapSet.new(),
            visited_modules: MapSet.new(),
            on_complete: :noop

  def start_link do
    Agent.start_link(fn -> %Tracker{} end, name: __MODULE__)
  end

  def notify_on_complete(pid) do
    Agent.update(__MODULE__, fn state -> %{state | on_complete: pid} end)
  end

  def set_total(modules) do
    total =
      modules
      |> Enum.flat_map(& &1.all_koans())
      |> Enum.count()

    Agent.update(__MODULE__, fn _ -> %Tracker{total: total} end)
  end

  def completed(module, koan) do
    Agent.update(__MODULE__, &mark_koan_completed(&1, module, koan))

    if complete?() do
      Agent.cast(__MODULE__, fn state ->
        send(state.on_complete, {self(), :complete})
        state
      end)
    end
  end

  def wait_until_complete do
    pid = Process.whereis(Tracker)

    receive do
      {^pid, :complete} -> :ok
    end
  end

  defp mark_koan_completed(state, module, koan) do
    %{
      state
      | koans: MapSet.put(state.koans, koan),
        visited_modules: MapSet.put(state.visited_modules, module)
    }
  end

  def visited do
    summarize()[:visited_modules]
  end

  def complete? do
    %{total: total, current: completed} = summarize()
    total == completed
  end

  def summarize do
    state = Agent.get(__MODULE__, & &1)

    %{
      total: state.total,
      current: MapSet.size(state.koans),
      visited_modules: MapSet.to_list(state.visited_modules)
    }
  end
end
