require "protobuf_descriptor/enum_descriptor"
require "protobuf_descriptor/message_descriptor"
require "protobuf_descriptor/service_descriptor"

require "active_support"
require "active_support/core_ext/object/blank"
require "active_support/core_ext/string/inflections"

class ProtobufDescriptor
  # Describes a complete .proto file.
  #
  # See {+FileDescriptorProto+}[https://code.google.com/p/protobuf/source/browse/trunk/src/google/protobuf/descriptor.proto#56]
  class FileDescriptor
    # The parent FileDescriptorSet[link:ProtobufDescriptor.html]
    attr_reader :file_descriptor_set

    # The +FileDescriptorProto+ this +FileDescriptor+ is wrapping.
    attr_reader :file_descriptor_proto

    # List of the message types that are defined at the top level of this file,
    # as a NamedCollection of
    # MessageDescriptors[link:MessageDescriptor.html]
    attr_reader :message_type

    # List of the enum types that are defined at the top level of this file,
    # as a NamedCollection of
    # EnumDescriptors[link:EnumDescriptor.html]
    attr_reader :enum_type

    # List of the services that are defined at the top level of this file, as a
    # NamedCollection of
    # ServiceDescriptors[link:ServiceDescriptor.html]
    attr_reader :service

    def initialize(file_descriptor_set, file_descriptor_proto) #:nodoc:
      # This is basically a parent pointer.
      @file_descriptor_set = file_descriptor_set
      @file_descriptor_proto = file_descriptor_proto

      @message_type = ProtobufDescriptor::NamedCollection.new(
          file_descriptor_proto.message_type.map { |m|
            ProtobufDescriptor::MessageDescriptor.new(self, m)
          })
      @enum_type = ProtobufDescriptor::NamedCollection.new(
          file_descriptor_proto.enum_type.map { |m|
            ProtobufDescriptor::EnumDescriptor.new(self, m)
          })
      @service = ProtobufDescriptor::NamedCollection.new(
          file_descriptor_proto.service.map { |m|
            ProtobufDescriptor::ServiceDescriptor.new(self, m)
          })
    end

    alias_method :parent, :file_descriptor_set
    alias_method :message_types, :message_type
    alias_method :messages, :message_type
    alias_method :enum_types, :enum_type
    alias_method :enums, :enum_types
    alias_method :services, :service

    # Set of all top-level messages, enums and services that are defined inside
    # of this file
    def children
      @children ||= ProtobufDescriptor::NamedCollection.new(
          @message_type.collection + @enum_type.collection + @service.collection)
    end

    # Filename relative to root of source tree.
    def name
      file_descriptor_proto.name
    end

    # The package name defined by the .proto file. E.G. "foo", "foo.bar", etc.
    def package
      file_descriptor_proto.package
    end

    # The Java package where classes generated from this .proto will be
    # placed. By default, the proto package is used, but this is often
    # inappropriate because proto packages do not normally start with backwards
    # domain names.
    def java_package
      if file_descriptor_proto.has_field?(:options) && file_descriptor_proto.options.java_package.present?
        return file_descriptor_proto.options.java_package
      else
        return file_descriptor_proto.package
      end
    end

    # If set, all the classes from the .proto file are wrapped in a single
    # outer class with the given name. This applies to both Proto1
    # (equivalent to the old "--one_java_file" option) and Proto2 (where
    # a .proto always translates to a single class, but you may want to
    # explicitly choose the class name).
    def java_outer_classname
      if file_descriptor_proto.has_field?(:options) && file_descriptor_proto.options.java_multiple_files.present?
        return nil
      elsif file_descriptor_proto.has_field?(:options) && file_descriptor_proto.options.java_outer_classname.present?
        return file_descriptor_proto.options.java_outer_classname
      else
        basename = name.split('/').last
        basename = basename.gsub('.proto', '')
        return basename.camelize
      end
    end

    # Returns the fully qualified name, as used by the +resolve_type+ methods.
    def fully_qualified_name
      return ".#{self.package}"
    end

    # Returns the fully qualified Java class name.
    def fully_qualified_java_name
      return [java_package, java_outer_classname].compact.join('.')
    end

    # Returns the fully qualified Java name as Wire would generate it. Wire
    # never wraps the definitions in a class named after the .proto file
    # (essentially behaving as if +java_outer_classname+ were always true)
    def fully_qualified_wire_name
      return java_package
    end

    # Returns the fully qualified Ruby class name as generated by various
    # protobuf gems.
    def fully_qualified_ruby_name
      return "::#{self.package.gsub('.', '::')}"
    end
  end
end