defmodule Kernel.ParallelRequire do
  @moduledoc """
  A module responsible for requiring files in parallel.
  """

  @doc """
  Requires the given files.

  A callback that will be invoked with each file, or a keyword list of `callbacks` can be provided:

    * `:each_file` - invoked with each file

    * `:each_module` - invoked with file, module name, and binary code

  Returns the modules generated by each required file.
  """
  def files(files, callbacks \\ [])

  def files(files, callback) when is_function(callback, 1) do
    files(files, [each_file: callback])
  end

  def files(files, callbacks) when is_list(callbacks) do
    compiler_pid = self()
    :elixir_code_server.cast({:reset_warnings, compiler_pid})
    schedulers = max(:erlang.system_info(:schedulers_online), 2)
    result = spawn_requires(files, [], callbacks, schedulers, [])

    # In case --warning-as-errors is enabled and there was a warning,
    # compilation status will be set to error.
    case :elixir_code_server.call({:compilation_status, compiler_pid}) do
      :ok ->
        result
      :error ->
        IO.puts :stderr, "\nExecution failed due to warnings while using the --warnings-as-errors option"
        exit({:shutdown, 1})
    end
  end

  defp spawn_requires([], [], _callbacks, _schedulers, result), do: result

  defp spawn_requires([], waiting, callbacks, schedulers, result) do
    wait_for_messages([], waiting, callbacks, schedulers, result)
  end

  defp spawn_requires(files, waiting, callbacks, schedulers, result) when length(waiting) >= schedulers do
    wait_for_messages(files, waiting, callbacks, schedulers, result)
  end

  defp spawn_requires([file | files], waiting, callbacks, schedulers, result) do
    parent = self()
    {pid, ref} = :erlang.spawn_monitor fn ->
      :erlang.put(:elixir_compiler_pid, parent)
      :erlang.put(:elixir_compiler_file, file)

      exit(try do
        new = Code.require_file(file) || []
        {:required, Enum.map(new, &elem(&1, 0)), file}
      catch
        kind, reason ->
          {:failure, kind, reason, System.stacktrace}
      end)
    end

    spawn_requires(files, [{pid, ref} | waiting], callbacks, schedulers, result)
  end

  defp wait_for_messages(files, waiting, callbacks, schedulers, result) do
    receive do
      {:DOWN, ref, :process, pid, status} ->
        tuple = {pid, ref}
        if tuple in waiting do
          waiting = List.delete(waiting, tuple)

          case status do
            {:required, mods, file} ->
              if each_file_callback = callbacks[:each_file] do
                each_file_callback.(file)
              end

              spawn_requires(files, waiting, callbacks, schedulers, mods ++ result)

            {:failure, kind, reason, stacktrace} ->
              :erlang.raise(kind, reason, stacktrace)

            other ->
              :erlang.raise(:exit, other, [])
          end
        else
          spawn_requires(files, waiting, callbacks, schedulers, result)
        end

      {:module_available, child, ref, file, module, binary} ->
        if each_module_callback = callbacks[:each_module] do
          each_module_callback.(file, module, binary)
        end

        send(child, {ref, :ack})
        spawn_requires(files, waiting, callbacks, schedulers, result)

      {:struct_available, _} ->
        spawn_requires(files, waiting, callbacks, schedulers, result)

      {:waiting, _, child, ref, _, _} ->
        send(child, {ref, :not_found})
        spawn_requires(files, waiting, callbacks, schedulers, result)
    end
  end
end
