# frozen_string_literal: true
require "graphql/relay/type_extensions"

module GraphQL
  # The parent for all type classes.
  class BaseType
    include GraphQL::Define::NonNullWithBang
    include GraphQL::Define::InstanceDefinable
    include GraphQL::Relay::TypeExtensions

    accepts_definitions :name, :description,
        :introspection,
        :default_scalar,
        :default_relay,
        {
          connection: GraphQL::Define::AssignConnection,
          global_id_field: GraphQL::Define::AssignGlobalIdField,
        }

    ensure_defined(:graphql_name, :name, :description, :introspection?, :default_scalar?)

    attr_accessor :ast_node

    def initialize
      @introspection = false
      @default_scalar = false
      @default_relay = false
    end

    def initialize_copy(other)
      super
      # Reset these derived defaults
      @connection_type = nil
      @edge_type = nil
    end

    # @return [String] the name of this type, must be unique within a Schema
    attr_reader :name
    # Future-compatible alias
    # @see {GraphQL::SchemaMember}
    alias :graphql_name :name
    # Future-compatible alias
    # @see {GraphQL::SchemaMember}
    alias :graphql_definition :itself

    def name=(name)
      GraphQL::NameValidator.validate!(name)
      @name = name
    end

    # @return [String, nil] a description for this type
    attr_accessor :description

    # @return [Boolean] Is this type a predefined introspection type?
    def introspection?
      @introspection
    end

    # @return [Boolean] Is this type a built-in scalar type? (eg, `String`, `Int`)
    def default_scalar?
      @default_scalar
    end

    # @return [Boolean] Is this type a built-in Relay type? (`Node`, `PageInfo`)
    def default_relay?
      @default_relay
    end

    # @api private
    attr_writer :introspection, :default_scalar, :default_relay

    # @param other [GraphQL::BaseType] compare to this object
    # @return [Boolean] are these types equivalent? (incl. non-null, list)
    # @see {ModifiesAnotherType#==} for override on List & NonNull types
    def ==(other)
      other.is_a?(GraphQL::BaseType) && self.name == other.name
    end

    # If this type is modifying an underlying type,
    # return the underlying type. (Otherwise, return `self`.)
    def unwrap
      self
    end

    # @return [GraphQL::NonNullType] a non-null version of this type
    def to_non_null_type
      GraphQL::NonNullType.new(of_type: self)
    end

    # @return [GraphQL::ListType] a list version of this type
    def to_list_type
      GraphQL::ListType.new(of_type: self)
    end

    module ModifiesAnotherType
      def unwrap
        self.of_type.unwrap
      end

      def ==(other)
        other.is_a?(ModifiesAnotherType) && other.of_type == of_type
      end
    end

    # Find out which possible type to use for `value`.
    # Returns self if there are no possible types (ie, not Union or Interface)
    def resolve_type(value, ctx)
      self
    end

    # Print the human-readable name of this type using the query-string naming pattern
    def to_s
      name
    end

    alias :inspect :to_s
    alias :to_type_signature :to_s

    def valid_isolated_input?(value)
      valid_input?(value, GraphQL::Query::NullContext)
    end

    def validate_isolated_input(value)
      validate_input(value, GraphQL::Query::NullContext)
    end

    def coerce_isolated_input(value)
      coerce_input(value, GraphQL::Query::NullContext)
    end

    def coerce_isolated_result(value)
      coerce_result(value, GraphQL::Query::NullContext)
    end

    def valid_input?(value, ctx = nil)
      if ctx.nil?
        warn_deprecated_coerce("valid_isolated_input?")
        ctx = GraphQL::Query::NullContext
      end

      validate_input(value, ctx).valid?
    end

    def validate_input(value, ctx = nil)
      if ctx.nil?
        warn_deprecated_coerce("validate_isolated_input")
        ctx = GraphQL::Query::NullContext
      end

      if value.nil?
        GraphQL::Query::InputValidationResult.new
      else
        validate_non_null_input(value, ctx)
      end
    end

    def coerce_input(value, ctx = nil)
      if value.nil?
        nil
      else
        if ctx.nil?
          warn_deprecated_coerce("coerce_isolated_input")
          ctx = GraphQL::Query::NullContext
        end
        coerce_non_null_input(value, ctx)
      end
    end

    def coerce_result(value, ctx)
      raise GraphQL::RequiredImplementationMissingError
    end

    # Types with fields may override this
    # @param name [String] field name to lookup for this type
    # @return [GraphQL::Field, nil]
    def get_field(name)
      nil
    end

    # During schema definition, types can be defined inside procs or as strings.
    # This function converts it to a type instance
    # @return [GraphQL::BaseType]
    def self.resolve_related_type(type_arg)
      case type_arg
      when Proc
        # lazy-eval it, then try again
        resolve_related_type(type_arg.call)
      when String
        # Get a constant by this name
        resolve_related_type(Object.const_get(type_arg))
      else
        if type_arg.respond_to?(:graphql_definition)
          type_arg.graphql_definition
        else
          type_arg
        end
      end
    end

    # Return a GraphQL string for the type definition
    # @param schema [GraphQL::Schema]
    # @param printer [GraphQL::Schema::Printer]
    # @see {GraphQL::Schema::Printer#initialize for additional options}
    # @return [String] type definition
    def to_definition(schema, printer: nil, **args)
      printer ||= GraphQL::Schema::Printer.new(schema, **args)
      printer.print_type(self)
    end

    # Returns true if this is a non-nullable type. A nullable list of non-nullables is considered nullable.
    def non_null?
      false
    end

    # Returns true if this is a list type. A non-nullable list is considered a list.
    def list?
      false
    end

    private

    def warn_deprecated_coerce(alt_method_name)
      warn("Coercing without a context is deprecated; use `#{alt_method_name}` if you don't want context-awareness")
    end
  end
end
