defmodule Twirp.Protoc.CLI do
  @moduledoc false
  # Almost all of this generation stuff is lifted from the elixr protobuf library.
  # I don't love the way its implemented but it was the fastest path forward for
  # supporting generation of services. I'm going to revisit in the future
  # because I barely understand how this code works.

  def main(_) do
    # https://groups.google.com/forum/#!topic/elixir-lang-talk/T5enez_BBTI
    :io.setopts(:standard_io, encoding: :latin1)
    bin = IO.binread(:all)
    request = Protobuf.Decoder.decode(bin, Google.Protobuf.Compiler.CodeGeneratorRequest)

    ctx =
      %Protobuf.Protoc.Context{}
      |> Protobuf.Protoc.CLI.parse_params(request.parameter || "")
      |> Protobuf.Protoc.CLI.find_types(request.proto_file)

    files =
      request.proto_file
      |> Enum.filter(fn desc -> Enum.member?(request.file_to_generate, desc.name) end)
      |> Enum.map(&convert_to_maps/1)
      |> Enum.map(&add_comments_to_methods/1)
      |> Enum.map(fn desc -> Twirp.Protoc.Generator.generate(ctx, desc) end)

    response = Google.Protobuf.Compiler.CodeGeneratorResponse.new(file: files)
    IO.binwrite(Protobuf.Encoder.encode(response))
  end

  defp add_comments_to_methods(desc) do
    import Access

    # Protobuf elixir has no way to find the actual field numbers. But we need
    # them in order to find the correct service and rpc definition. It just so
    # happens that the "service" field on the descriptor is number 6 and the method descriptor
    # is number 2. So we explicitly check for that and then move on.
    comments =
      desc.source_code_info.location
      |> Enum.reject(fn loc -> loc.leading_comments == nil end)
      |> Enum.filter(fn loc -> match?([6, _, 2, _], loc.path) end)
      |> Enum.map(fn %{path: [6, service, 2, method], leading_comments: comments} -> {service, method, comments} end)

    Enum.reduce(comments, desc, fn {service, method, comment}, desc ->
      desc
      |> put_in([:service, at(service), :method, at(method), :comments], comment)
    end)
  end

  defp convert_to_maps(desc) do
    services = Enum.map(desc.service, fn s ->
      methods = Enum.map(s.method, fn m ->
        Map.from_struct(m)
      end)

      s
      |> Map.put(:method, methods)
      |> Map.from_struct
    end)

    desc
    |> Map.put(:service, services)
    |> Map.from_struct()
  end
end
