# typed: strict
# frozen_string_literal: true
require "parlour"

begin
  require "google/protobuf"
rescue LoadError
  return
end

module Tapioca
  module Compilers
    module Dsl
      # `Tapioca::Compilers::Dsl::Protobuf` decorates RBI files for subclasses of
      # [`Google::Protobuf::MessageExts`](https://github.com/protocolbuffers/protobuf/tree/master/ruby).
      #
      # For example, with the following "cart.rb" file:
      #
      # ~~~rb
      # Google::Protobuf::DescriptorPool.generated_pool.build do
      #   add_file("cart.proto", :syntax => :proto3) do
      #     add_message "MyCart" do
      #       optional :shop_id, :int32, 1
      #       optional :customer_id, :int64, 2
      #       optional :number_value, :double, 3
      #       optional :string_value, :string, 4
      #     end
      #   end
      # end
      # ~~~
      #
      # this generator will produce the RBI file `cart.rbi` with the following content:
      #
      # ~~~rbi
      # # cart.rbi
      # # typed: strong
      # class Cart
      #   sig { returns(Integer) }
      #   def customer_id; end
      #
      #   sig { params(month: Integer).returns(Integer) }
      #   def customer_id=(value); end
      #
      #   sig { returns(Integer) }
      #   def shop_id; end
      #
      #   sig { params(value: Integer).returns(Integer) }
      #   def shop_id=(value); end
      #
      #   sig { returns(String) }
      #   def string_value; end
      #
      #   sig { params(value: String).returns(String) }
      #   def string_value=(value); end
      #
      #
      #   sig { returns(Float) }
      #   def number_value; end
      #
      #   sig { params(value: Float).returns(Float) }
      #   def number_value=(value); end
      # end
      # ~~~
      class Protobuf < Base
        # Parlour doesn't support type members out of the box, so adding the
        # ability to do that here. This should be upstreamed.
        class TypeMember < Parlour::RbiGenerator::RbiObject
          extend T::Sig

          sig { params(other: Object).returns(T::Boolean) }
          def ==(other)
            TypeMember === other && name == other.name
          end

          sig do
            override
              .params(indent_level: Integer, options: Parlour::RbiGenerator::Options)
              .returns(T::Array[String])
          end
          def generate_rbi(indent_level, options)
            [options.indented(indent_level, "#{name} = type_member")]
          end

          sig do
            override
              .params(others: T::Array[Parlour::RbiGenerator::RbiObject])
              .returns(T::Boolean)
          end
          def mergeable?(others)
            others.all? { |other| self == other }
          end

          sig { override.params(others: T::Array[Parlour::RbiGenerator::RbiObject]).void }
          def merge_into_self(others); end

          sig { override.returns(String) }
          def describe
            "Type Member (#{name})"
          end
        end

        class Field < T::Struct
          prop :name, String
          prop :type, String
          prop :init_type, String
          prop :default, String

          extend T::Sig

          sig { returns(Parlour::RbiGenerator::Parameter) }
          def to_init
            Parlour::RbiGenerator::Parameter.new("#{name}:", type: init_type, default: default)
          end
        end

        extend T::Sig

        sig do
          override.params(
            root: Parlour::RbiGenerator::Namespace,
            constant: Module
          ).void
        end
        def decorate(root, constant)
          root.path(constant) do |klass|
            if constant == Google::Protobuf::RepeatedField
              create_type_members(klass, "Elem")
            elsif constant == Google::Protobuf::Map
              create_type_members(klass, "Key", "Value")
            else
              descriptor = T.let(T.unsafe(constant).descriptor, Google::Protobuf::Descriptor)
              fields = descriptor.map { |desc| create_descriptor_method(klass, desc) }
              fields.sort_by!(&:name)

              create_method(klass, "initialize", parameters: fields.map!(&:to_init))
            end
          end
        end

        sig { override.returns(T::Enumerable[Module]) }
        def gather_constants
          marker = Google::Protobuf::MessageExts::ClassMethods
          results = T.cast(ObjectSpace.each_object(marker).to_a, T::Array[Module])
          results.any? ? results + [Google::Protobuf::RepeatedField, Google::Protobuf::Map] : []
        end

        private

        sig { params(klass: Parlour::RbiGenerator::Namespace, names: String).void }
        def create_type_members(klass, *names)
          klass.create_extend("T::Generic")

          names.each do |name|
            klass.children << TypeMember.new(klass.generator, name)
          end
        end

        sig do
          params(
            descriptor: Google::Protobuf::FieldDescriptor
          ).returns(String)
        end
        def type_of(descriptor)
          case descriptor.type
          when :enum
            descriptor.subtype.enummodule.name
          when :message
            descriptor.subtype.msgclass.name
          when :int32, :int64, :uint32, :uint64
            "Integer"
          when :double, :float
            "Float"
          when :bool
            "T::Boolean"
          when :string, :bytes
            "String"
          else
            "T.untyped"
          end
        end

        sig { params(descriptor: Google::Protobuf::FieldDescriptor).returns(Field) }
        def field_of(descriptor)
          if descriptor.label == :repeated
            # Here we're going to check if the submsg_name is named according to
            # how Google names map entries.
            # https://github.com/protocolbuffers/protobuf/blob/f82e26/ruby/ext/google/protobuf_c/defs.c#L1963-L1966
            if descriptor.submsg_name.to_s.end_with?("_MapEntry_#{descriptor.name}")
              key = descriptor.subtype.lookup('key')
              value = descriptor.subtype.lookup('value')

              key_type = type_of(key)
              value_type = type_of(value)
              type = "Google::Protobuf::Map[#{key_type}, #{value_type}]"

              default_args = [key.type.inspect, value.type.inspect]
              default_args << value_type if %i[enum message].include?(value.type)

              Field.new(
                name: descriptor.name,
                type: type,
                init_type: "T.any(#{type}, T::Hash[#{key_type}, #{value_type}])",
                default: "Google::Protobuf::Map.new(#{default_args.join(', ')})"
              )
            else
              elem_type = type_of(descriptor)
              type = "Google::Protobuf::RepeatedField[#{elem_type}]"

              default_args = [descriptor.type.inspect]
              default_args << elem_type if %i[enum message].include?(descriptor.type)

              Field.new(
                name: descriptor.name,
                type: type,
                init_type: "T.any(#{type}, T::Array[#{elem_type}])",
                default: "Google::Protobuf::RepeatedField.new(#{default_args.join(', ')})"
              )
            end
          else
            type = type_of(descriptor)

            Field.new(
              name: descriptor.name,
              type: type,
              init_type: type,
              default: "nil"
            )
          end
        end

        sig do
          params(
            klass: Parlour::RbiGenerator::Namespace,
            desc: Google::Protobuf::FieldDescriptor,
          ).returns(Field)
        end
        def create_descriptor_method(klass, desc)
          field = field_of(desc)

          create_method(
            klass,
            field.name,
            return_type: field.type
          )

          create_method(
            klass,
            "#{field.name}=",
            parameters: [
              Parlour::RbiGenerator::Parameter.new("value", type: field.type),
            ],
            return_type: field.type
          )

          field
        end
      end
    end
  end
end