defmodule Serum.IOProxy do
  @moduledoc false

  use GenServer

  message_categories = [
    debug: {[:light_black_background, :black], [:light_black]},
    error: {[:red_background, :light_white], [:light_white]},
    gen: {[:light_green], []},
    info: {[:light_black_background, :white], []},
    mkdir: {[:light_cyan], []},
    plugin: {[:light_magenta], []},
    read: {[:light_yellow], []},
    theme: {[:light_magenta], []},
    warn: {[:yellow_background, :black], [:yellow]}
  ]

  @type config :: %{mute_msg: boolean(), mute_err: boolean()}

  @spec start_link(term()) :: GenServer.on_start()
  def start_link(_args) do
    GenServer.start_link(__MODULE__, [], name: __MODULE__)
  end

  @doc "Gets the current configuration of `Serum.IOProxy`."
  @spec config() :: {:ok, config()}
  def config do
    GenServer.call(__MODULE__, :get_config)
  end

  @doc """
  Configures `Serum.IOProxy`.

  ## Options

  - `mute_err` (boolean): Controls whether outputs to `:stderr` must be
    suppressed. Defaults to `false`.
  - `mute_msg` (boolean): Controls whether outputs to `:stdio` must be
    suppressed. Defaults to `false`.
  """
  @spec config(keyword()) :: :ok
  def config(options) when is_list(options) do
    GenServer.call(__MODULE__, {:set_config, options})
  end

  @doc """
  Prints a message to the standard output.

  Available categories are:
  `#{message_categories |> Keyword.keys() |> inspect()}`
  """
  @spec put_msg(atom(), IO.ANSI.ansidata()) :: :ok
  def put_msg(category, msg) do
    GenServer.call(__MODULE__, {:put_msg, category, msg})
  end

  @doc """
  Prints a message to the standard error output.

  Available categories are:
  `#{message_categories |> Keyword.keys() |> inspect()}`
  """
  @spec put_err(atom(), IO.ANSI.ansidata()) :: :ok
  def put_err(category, msg) do
    GenServer.call(__MODULE__, {:put_err, category, msg})
  end

  @impl GenServer
  def init(_args) do
    prefix = if IO.ANSI.enabled?(), do: "\r", else: ""

    {:ok, %{mute_msg: false, mute_err: false, prefix: prefix}}
  end

  @impl GenServer
  def handle_call(request, from, state)
  def handle_call(:get_config, _, state), do: {:reply, {:ok, state}, state}

  def handle_call({:set_config, conf}, _, state) do
    conf_map = conf |> Map.new() |> Map.take([:mute_msg, :mute_err])

    {:reply, :ok, Map.merge(state, conf_map)}
  end

  def handle_call({:put_msg, category, msg}, _, state) do
    unless state.mute_msg do
      IO.puts([state.prefix, format_message(category, msg)])
    end

    {:reply, :ok, state}
  end

  def handle_call({:put_err, category, msg}, _, state) do
    unless state.mute_err do
      IO.puts(:stderr, [state.prefix, format_message(category, msg)])
    end

    {:reply, :ok, state}
  end

  max_length =
    message_categories
    |> Enum.map(fn {category, _} -> to_string(category) end)
    |> Enum.map(&String.length/1)
    |> Enum.max()

  @spec format_message(atom(), IO.ANSI.ansidata()) :: IO.chardata()
  defp format_message(category, msg)

  Enum.each(message_categories, fn {category, {head_style, body_style}} ->
    cat_str = category |> to_string() |> String.upcase()
    header = [head_style, String.pad_leading(cat_str, max_length + 1), ?\s]
    body_fmt = [body_style, "~ts"]

    defp format_message(unquote(category), msg) do
      header = unquote(header) |> IO.ANSI.format() |> IO.iodata_to_binary()
      body_fmt = unquote(body_fmt) |> IO.ANSI.format() |> IO.iodata_to_binary()

      formatted_msg =
        msg
        |> IO.ANSI.format()
        |> IO.iodata_to_binary()
        |> format_newlines()

      [header, ?\s, :io_lib.format(body_fmt, [formatted_msg])]
    end
  end)

  @spec format_newlines(binary()) :: IO.chardata()
  defp format_newlines(msg) do
    lines = String.split(msg, ~r/\r?\n/)
    indent = String.duplicate(" ", unquote(max_length) + 3)

    Enum.intersperse(lines, ["\n", indent])
  end
end
