module GraphQL
  # Used to convert your {GraphQL::Schema} to a GraphQL schema string
  #
  # @example print your schema to standard output
  #   Schema = GraphQL::Schema.new(query: QueryType)
  #   puts GraphQL::Schema::Printer.print_schema(Schema)
  #
  module Schema::Printer
    extend self

    # Return a GraphQL schema string for the defined types in the schema
    # @param schema [GraphQL::Schema]
    def print_schema(schema)
      print_filtered_schema(schema, method(:is_defined_type))
    end

    # Return the GraphQL schema string for the introspection type system
    def print_introspection_schema
      query_root = ObjectType.define do
        name "Query"
      end
      schema = Schema.new(query: query_root)
      print_filtered_schema(schema, method(:is_introspection_type))
    end

    private

    def print_filtered_schema(schema, type_filter)
      types = schema.types.values.select{ |type| type_filter.call(type) }.sort_by(&:name)
      types.map{ |type| print_type(type) }.join("\n\n")
    end

    BUILTIN_SCALARS = Set.new(["String", "Boolean", "Int", "Float", "ID"])
    private_constant :BUILTIN_SCALARS

    def is_introspection_type(type)
      type.name.start_with?("__")
    end

    def is_defined_type(type)
      !is_introspection_type(type) && !BUILTIN_SCALARS.include?(type.name)
    end

    def print_type(type)
      TypeKindPrinters::STRATEGIES.fetch(type.kind).print(type)
    end

    module TypeKindPrinters
      module FieldPrinter
        def print_fields(type)
          type.fields.values.map{ |field| "  #{field.name}#{print_args(field)}: #{field.type}" }.join("\n")
        end

        def print_args(field)
          return if field.arguments.empty?
          "(#{field.arguments.values.map{ |arg| print_input_value(arg) }.join(", ")})"
        end

        def print_input_value(arg)
          default_string = " = #{print_value(arg.default_value, arg.type)}" unless arg.default_value.nil?
          "#{arg.name}: #{arg.type.to_s}#{default_string}"
        end

        def print_value(value, type)
          case type
          when FLOAT_TYPE
            value.to_f.inspect
          when INT_TYPE
            value.to_i.inspect
          when BOOLEAN_TYPE
            (!!value).inspect
          when ScalarType, ID_TYPE, STRING_TYPE
            value.to_s.inspect
          when EnumType
            value.to_s
          when InputObjectType
            fields = value.to_h.map{ |field_name, field_value|
              field_type = type.input_fields.fetch(field_name.to_s).type
              "#{field_name}: #{print_value(field_value, field_type)}"
            }.join(", ")
            "{ #{fields} }"
          when NonNullType
            print_value(value, type.of_type)
          when ListType
            "[#{value.to_a.map{ |v| print_value(v, type.of_type) }.join(", ")}]"
          else
            raise NotImplementedError, "Unexpected value type #{type.inspect}"
          end
        end
      end

      class ScalarPrinter
        def self.print(type)
          "scalar #{type.name}"
        end
      end

      class ObjectPrinter
        extend FieldPrinter
        def self.print(type)
          implementations = " implements #{type.interfaces.map(&:to_s).join(", ")}" unless type.interfaces.empty?
          "type #{type.name}#{implementations} {\n#{print_fields(type)}\n}"
        end
      end

      class InterfacePrinter
        extend FieldPrinter
        def self.print(type)
          "interface #{type.name} {\n#{print_fields(type)}\n}"
        end
      end

      class UnionPrinter
        def self.print(type)
          "union #{type.name} = #{type.possible_types.map(&:to_s).join(" | ")}\n}"
        end
      end

      class EnumPrinter
        def self.print(type)
          values = type.values.values.map{ |v| "  #{v.name}" }.join("\n")
          "enum #{type.name} {\n#{values}\n}"
        end
      end

      class InputObjectPrinter
        extend FieldPrinter
        def self.print(type)
          fields = type.input_fields.values.map{ |field| "  #{print_input_value(field)}" }.join("\n")
          "input #{type.name} {\n#{fields}\n}"
        end
      end

      STRATEGIES = {
        GraphQL::TypeKinds::SCALAR =>       ScalarPrinter,
        GraphQL::TypeKinds::OBJECT =>       ObjectPrinter,
        GraphQL::TypeKinds::INTERFACE =>    InterfacePrinter,
        GraphQL::TypeKinds::UNION =>        UnionPrinter,
        GraphQL::TypeKinds::ENUM =>         EnumPrinter,
        GraphQL::TypeKinds::INPUT_OBJECT => InputObjectPrinter,
      }
    end
    private_constant :TypeKindPrinters
  end
end
