defmodule Protox.Generate do
  @moduledoc false

  defmodule FileContent do
    @moduledoc false
    defstruct([:name, :content])
  end

  def generate_module_code(files, output_path, multiple_files, include_paths, opts \\ [])
      when is_list(files) and is_binary(output_path) and is_boolean(multiple_files) do
    paths =
      case include_paths do
        [] -> nil
        _ -> Enum.map(include_paths, &Path.expand/1)
      end

    case launch_protoc(files, paths) do
      {:ok, file_descriptor_set} ->
        {:ok, definition} = Protox.Parse.parse(file_descriptor_set, opts)
        code = Protox.Define.define(definition, opts)
        {:ok, generate_files(output_path, code, multiple_files)}

      {:error, msg} ->
        {:error, msg}
    end
  end

  # -- Private

  defp launch_protoc(files, paths) do
    files
    |> Enum.map(&Path.expand/1)
    |> Protox.Protoc.run(paths)
  end

  defp generate_files(output_path, code, false = _muliple_files) do
    [
      %FileContent{
        name: output_path,
        content: generate_file_content(code)
      }
    ]
  end

  defp generate_files(output_path, code, true = _muliple_files) do
    # Protox.Define.define outputs a block, we thus have to extract the content
    # on which we can iterate.
    {:__block__, _, block} = code

    Enum.map(block, fn {:defmodule, _, [module_name | _]} = module_code ->
      snake_module_name =
        module_name
        |> Module.split()
        |> Enum.map_join("_", &Macro.underscore/1)

      %FileContent{
        name: Path.join(output_path, snake_module_name <> ".ex"),
        content: generate_file_content(module_code)
      }
    end)
  end

  defp generate_file_content(code) do
    [
      "# Code generated by protox. Don't edit.\n",
      "# credo:disable-for-this-file\n",
      code |> Macro.to_string() |> Code.format_string!(),
      "\n"
    ]
  end
end
