defmodule Igniter.Installer.Loading do
  @moduledoc """
  Utilities for doing operations with loading spinners.
  """

  @spinners "⠁⠂⠄⡀⡁⡂⡄⡅⡇⡏⡗⡧⣇⣏⣗⣧⣯⣷⣿⢿⣻⢻⢽⣹⢹⢸⠸⢘⠘⠨⢈⠈⠐⠠⢀" |> String.graphemes()

  @doc """
  Runs the function with a loading spinner, suppressing all output.
  """
  def with_spinner(name, fun, opts \\ []) do
    if opts[:verbose?] do
      Mix.shell().info(name <> ":")
      result = fun.()
      Mix.shell().info(name <> ": " <> "#{IO.ANSI.green()}✔#{IO.ANSI.reset()}")
      result
    else
      shell = Mix.shell()

      # I don't understand why I have to do this
      if !Code.ensure_loaded?(Mix.Shell.ActuallyQuiet) do
        Code.eval_quoted(
          quote do
            defmodule Mix.Shell.ActuallyQuiet do
              @moduledoc false
              @behaviour Mix.Shell

              def print_app, do: :ok

              def info(_message), do: :ok

              def error(message) do
                # Instead of discarding, send to stderr so it gets captured
                IO.puts(:stderr, message)
                :ok
              end

              def prompt(_message), do: :ok

              def yes?(_message, _options \\ []), do: :ok

              def cmd(command, opts \\ []) do
                Mix.Shell.cmd(command, opts, fn data -> data end)
              end
            end
          end
        )
      end

      {:ok, _} = Igniter.Installer.CaptureServer.start_link([])

      loader =
        if shell == Mix.Shell.IO do
          spawn_loader(name)
        end

      try do
        Mix.shell(Mix.Shell.ActuallyQuiet)

        {:ok, ref} =
          Igniter.Installer.CaptureServer.device_capture_on(:standard_error, :unicode, "")

        try do
          with_log(fn ->
            do_with_io(self(), [], fn ->
              fun.()
            end)
            |> elem(0)
          end)
          |> elem(0)
        catch
          kind, reason ->
            Process.put(:spinner_error, true)
            Mix.shell(shell)
            Mix.shell().info(Igniter.Installer.CaptureServer.device_output(:standard_error, ref))
            :erlang.raise(kind, reason, __STACKTRACE__)
        after
          Mix.shell(shell)
          Igniter.Installer.CaptureServer.device_capture_off(ref)
          GenServer.stop(Igniter.Installer.CaptureServer)
        end
      after
        if shell == Mix.Shell.IO do
          loader_ref = make_ref()
          error_occurred = Process.get(:spinner_error, false)

          stop_message =
            if error_occurred,
              do: {:stop_error, self(), loader_ref},
              else: {:stop_success, self(), loader_ref}

          send(loader, stop_message)

          receive do
            {:loader_stopped, ^loader_ref} ->
              nil
          after
            500 ->
              :ok
          end
        end
      end
    end
  end

  defp do_with_io(pid, options, fun) when is_pid(pid) do
    prompt_config = Keyword.get(options, :capture_prompt, true)
    encoding = Keyword.get(options, :encoding, :unicode)
    input = Keyword.get(options, :input, "")

    {:group_leader, original_gl} =
      Process.info(pid, :group_leader) || {:group_leader, Process.group_leader()}

    {:ok, capture_gl} = StringIO.open(input, capture_prompt: prompt_config, encoding: encoding)

    try do
      Process.group_leader(pid, capture_gl)
      do_capture_gl(capture_gl, fun)
    after
      Process.group_leader(pid, original_gl)
    end
  end

  defp do_capture_gl(string_io, fun) do
    try do
      fun.()
    catch
      kind, reason ->
        _ = StringIO.close(string_io)
        :erlang.raise(kind, reason, __STACKTRACE__)
    else
      result ->
        {:ok, {_input, output}} = StringIO.close(string_io)
        {result, output}
    end
  end

  def spawn_loader(task_name) do
    spawn_link(fn ->
      @spinners
      |> Stream.cycle()
      |> Stream.map(fn next ->
        receive do
          {:stop_success, pid, ref} ->
            IO.puts("\r\e[K" <> task_name <> " " <> "#{IO.ANSI.green()}✔#{IO.ANSI.reset()}")
            send(pid, {:loader_stopped, ref})
            exit(:normal)

          {:stop_error, pid, ref} ->
            IO.puts("\r\e[K" <> task_name <> " " <> "#{IO.ANSI.red()}✗#{IO.ANSI.reset()}")
            send(pid, {:loader_stopped, ref})
            exit(:normal)
        after
          0 ->
            IO.write("\r\e[K" <> task_name <> " " <> next)
            :timer.sleep(50)
        end
      end)
      |> Stream.run()
    end)
  end

  def with_log(opts \\ [], fun) when is_list(opts) do
    opts =
      if opts[:level] == :warn do
        IO.warn("level: :warn is deprecated, please use :warning instead")
        Keyword.put(opts, :level, :warning)
      else
        opts
      end

    {:ok, string_io} = StringIO.open("")

    try do
      ref = Igniter.Installer.CaptureServer.log_capture_on(self(), string_io, opts)

      try do
        fun.()
      after
        :ok = Logger.flush()
        :ok = Igniter.Installer.CaptureServer.log_capture_off(ref)
      end
    catch
      kind, reason ->
        _ = StringIO.close(string_io)
        :erlang.raise(kind, reason, __STACKTRACE__)
    else
      result ->
        {:ok, {_input, output}} = StringIO.close(string_io)
        {result, output}
    end
  end
end
