# frozen_string_literal: true

module Solargraph
  class ComplexType
    # An individual type signature. A complex type can consist of multiple
    # unique types.
    #
    class UniqueType
      include TypeMethods
      include Equality

      attr_reader :all_params, :subtypes, :key_types

      # @sg-ignore Fix "Not enough arguments to Module#protected"
      protected def equality_fields
        [@name, @all_params, @subtypes, @key_types]
      end

      # Create a UniqueType with the specified name and an optional substring.
      # The substring is the parameter section of a parametrized type, e.g.,
      # for the type `Array<String>`, the name is `Array` and the substring is
      # `<String>`.
      #
      # @param name [String] The name of the type
      # @param substring [String] The substring of the type
      # @param make_rooted [Boolean, nil]
      # @return [UniqueType]
      def self.parse name, substring = '', make_rooted: nil
        if name.start_with?(':::')
          raise ComplexTypeError, "Illegal prefix: #{name}"
        end
        if name.start_with?('::')
          name = name[2..-1]
          rooted = true
        elsif !can_root_name?(name)
          rooted = true
        else
          rooted = false
        end
        rooted = make_rooted unless make_rooted.nil?

        # @type [Array<ComplexType>]
        key_types = []
        # @type [Array<ComplexType>]
        subtypes = []
        parameters_type = nil
        unless substring.empty?
          subs = ComplexType.parse(substring[1..-2], partial: true)
          parameters_type = PARAMETERS_TYPE_BY_STARTING_TAG.fetch(substring[0])
          if parameters_type == :hash
            raise ComplexTypeError, "Bad hash type: name=#{name}, substring=#{substring}" unless !subs.is_a?(ComplexType) and subs.length == 2 and !subs[0].is_a?(UniqueType) and !subs[1].is_a?(UniqueType)
            key_types.concat(subs[0].map { |u| ComplexType.new([u]) })
            subtypes.concat(subs[1].map { |u| ComplexType.new([u]) })
          elsif parameters_type == :list && name == 'Hash'
            # Treat Hash<A, B> as Hash{A => B}
            if subs.length != 2
              raise ComplexTypeError, "Bad hash type: name=#{name}, substring=#{substring} - must have exactly two parameters"
            end
            key_types.concat(subs[0].map { |u| ComplexType.new([u]) })
            subtypes.concat(subs[1].map { |u| ComplexType.new([u]) })
          else
            subtypes.concat subs
          end
        end
        new(name, key_types, subtypes, rooted: rooted, parameters_type: parameters_type)
      end

      # @param name [String]
      # @param key_types [Array<ComplexType>]
      # @param subtypes [Array<ComplexType>]
      # @param rooted [Boolean]
      # @param parameters_type [Symbol, nil]
      def initialize(name, key_types = [], subtypes = [], rooted:, parameters_type: nil)
        if parameters_type.nil?
          raise "You must supply parameters_type if you provide parameters" unless key_types.empty? && subtypes.empty?
        end
        raise "Please remove leading :: and set rooted instead - #{name.inspect}" if name.start_with?('::')
        @name = name
        @parameters_type = parameters_type
        if implicit_union?
          @key_types = key_types.uniq
          @subtypes = subtypes.uniq
        else
          @key_types = key_types
          @subtypes = subtypes
        end
        @rooted = rooted
        @all_params = []
        @all_params.concat @key_types
        @all_params.concat @subtypes
      end

      def implicit_union?
        # @todo use api_map to establish number of generics in type;
        #   if only one is allowed but multiple are passed in, treat
        #   those as implicit unions
        ['Hash', 'Array', 'Set', '_ToAry', 'Enumerable', '_Each'].include?(name) && parameters_type != :fixed
      end

      def to_s
        tag
      end

      # @return [self]
      def simplify_literals
        transform do |t|
          next t unless t.literal?
          t.recreate(new_name: t.non_literal_name)
        end
      end

      def literal?
        non_literal_name != name
      end

      # @return [String]
      def non_literal_name
        @non_literal_name ||= determine_non_literal_name
      end

      # @return [String]
      def determine_non_literal_name
        # https://github.com/ruby/rbs/blob/master/docs/syntax.md
        #
        # _literal_ ::= _string-literal_
        #    | _symbol-literal_
        #    | _integer-literal_
        #    | `true`
        #    | `false`
        return name if name.empty?
        return 'NilClass' if name == 'nil'
        return 'Boolean' if ['true', 'false'].include?(name)
        return 'Symbol' if name[0] == ':'
        return 'String' if ['"', "'"].include?(name[0])
        return 'Integer' if name.match?(/^-?\d+$/)
        name
      end

      def eql?(other)
        self.class == other.class &&
          @name == other.name &&
          @key_types == other.key_types &&
          @subtypes == other.subtypes &&
          @rooted == other.rooted? &&
          @all_params == other.all_params &&
          @parameters_type == other.parameters_type
      end

      def ==(other)
        eql?(other)
      end

      def hash
        [self.class, @name, @key_types, @sub_types, @rooted, @all_params, @parameters_type].hash
      end

      # @return [Array<UniqueType>]
      def items
        [self]
      end

      # @return [String]
      def rbs_name
        if name == 'undefined'
          'untyped'
        elsif literal?
          name
        else
          rooted_name
        end
      end

      # @return [String]
      def desc
        rooted_tags
      end

      # @return [String]
      def to_rbs
        if duck_type?
          'untyped'
        elsif name == 'Boolean'
          'bool'
        elsif name.downcase == 'nil'
          'nil'
        elsif name == GENERIC_TAG_NAME
          all_params.first.name
        elsif ['Class', 'Module'].include?(name)
          rbs_name
        elsif ['Tuple', 'Array'].include?(name) && fixed_parameters?
          # tuples don't have a name; they're just [foo, bar, baz].
          if substring == '()'
            # but there are no zero element tuples, so we go with an array
            if rooted?
              '::Array[]'
            else
              'Array[]'
            end
          else
            # already generated surrounded by []
            parameters_as_rbs
          end
        else
          "#{rbs_name}#{parameters_as_rbs}"
        end
      end

      # @return [Boolean]
      def parameters?
        !all_params.empty?
      end

      # @param types [Array<UniqueType, ComplexType>]
      # @return [String]
      def rbs_union(types)
        if types.length == 1
          types.first.to_rbs
        else
          "(#{types.map(&:to_rbs).join(' | ')})"
        end
      end

      # @return [String]
      def parameters_as_rbs
        return '' unless parameters?

        return "[#{all_params.map(&:to_rbs).join(', ')}]" if key_types.empty?

        # handle, e.g., Hash[K, V] case
        key_types_str = rbs_union(key_types)
        subtypes_str = rbs_union(subtypes)
        "[#{key_types_str}, #{subtypes_str}]"
      end

      def generic?
        name == GENERIC_TAG_NAME || all_params.any?(&:generic?)
      end

      # @param api_map [ApiMap] The ApiMap that performs qualification
      # @param atype [ComplexType] type which may be assigned to this type
      def can_assign?(api_map, atype)
        logger.debug { "UniqueType#can_assign?(self=#{rooted_tags.inspect}, atype=#{atype.rooted_tags.inspect})" }
        downcasted_atype = atype.downcast_to_literal_if_possible
        out = downcasted_atype.all? do |autype|
          autype.name == name || api_map.super_and_sub?(name, autype.name)
        end
        logger.debug { "UniqueType#can_assign?(self=#{rooted_tags.inspect}, atype=#{atype.rooted_tags.inspect}) => #{out}" }
        out
      end

      # @return [UniqueType]
      def downcast_to_literal_if_possible
        SINGLE_SUBTYPE.fetch(rooted_tag, self)
      end

      # @param generics_to_resolve [Enumerable<String>]
      # @param context_type [UniqueType, nil]
      # @param resolved_generic_values [Hash{String => ComplexType, ComplexType::UniqueType}] Added to as types are encountered or resolved
      # @return [UniqueType, ComplexType]
      def resolve_generics_from_context generics_to_resolve, context_type, resolved_generic_values: {}
        if name == ComplexType::GENERIC_TAG_NAME
          type_param = subtypes.first&.name
          return self unless generics_to_resolve.include? type_param
          unless context_type.nil? || !resolved_generic_values[type_param].nil?
            new_binding = true
            resolved_generic_values[type_param] = context_type
          end
          if new_binding
            resolved_generic_values.transform_values! do |complex_type|
              complex_type.resolve_generics_from_context(generics_to_resolve, nil, resolved_generic_values: resolved_generic_values)
            end
          end
          return resolved_generic_values[type_param] || self
        end

        # @todo typechecking should complain when the method being called has no @yieldparam tag
        new_key_types = resolve_param_generics_from_context(generics_to_resolve, context_type, resolved_generic_values, &:key_types)
        new_subtypes = resolve_param_generics_from_context(generics_to_resolve, context_type, resolved_generic_values, &:subtypes)
        recreate(new_key_types: new_key_types, new_subtypes: new_subtypes)
      end

      # @param generics_to_resolve [Enumerable<String>]
      # @param context_type [UniqueType, nil]
      # @param resolved_generic_values [Hash{String => ComplexType}]
      # @yieldreturn [Array<ComplexType>]
      # @return [Array<ComplexType>]
      def resolve_param_generics_from_context(generics_to_resolve, context_type, resolved_generic_values)
        types = yield self
        types.each_with_index.flat_map do |ct, i|
          ct.items.flat_map do |ut|
            context_params = yield context_type if context_type
            if context_params && context_params[i]
              type_arg = context_params[i]
              type_arg.map do |new_unique_context_type|
                ut.resolve_generics_from_context generics_to_resolve, new_unique_context_type, resolved_generic_values: resolved_generic_values
              end
            else
              ut.resolve_generics_from_context generics_to_resolve, nil, resolved_generic_values: resolved_generic_values
            end
          end
        end
      end

      # Probe the concrete type for each of the generic type
      # parameters used in this type, and return a new type if
      # possible.
      #
      # @param definitions [Pin::Namespace, Pin::Method] The module/class/method which uses generic types
      # @param context_type [ComplexType] The receiver type
      # @return [UniqueType, ComplexType]
      def resolve_generics definitions, context_type
        return self if definitions.nil? || definitions.generics.empty?

        transform(name) do |t|
          if t.name == GENERIC_TAG_NAME
            generic_name = t.subtypes.first&.name
            idx = definitions.generics.index(generic_name)
            next t if idx.nil?
            if context_type.parameters_type == :hash
              if idx == 0
                next ComplexType.new(context_type.key_types)
              elsif idx == 1
                next ComplexType.new(context_type.subtypes)
              else
                next ComplexType::UNDEFINED
              end
            elsif context_type.all?(&:implicit_union?)
              if idx == 0 && !context_type.all_params.empty?
                ComplexType.new(context_type.all_params)
              else
                ComplexType::UNDEFINED
              end
            else
              context_type.all_params[idx] || definitions.generic_defaults[generic_name] || ComplexType::UNDEFINED
            end
          else
            t
          end
        end
      end

      # @yieldparam t [self]
      # @yieldreturn [self]
      # @return [Array<self>]
      def map &block
        [block.yield(self)]
      end

      # @return [Array<UniqueType>]
      def to_a
        [self]
      end

      # @param new_name [String, nil]
      # @param make_rooted [Boolean, nil]
      # @param new_key_types [Array<ComplexType>, nil]
      # @param rooted [Boolean, nil]
      # @param new_subtypes [Array<ComplexType>, nil]
      # @return [self]
      def recreate(new_name: nil, make_rooted: nil, new_key_types: nil, new_subtypes: nil)
        raise "Please remove leading :: and set rooted instead - #{new_name}" if new_name&.start_with?('::')

        new_name ||= name
        new_key_types ||= @key_types
        new_subtypes ||= @subtypes
        make_rooted = @rooted if make_rooted.nil?
        UniqueType.new(new_name, new_key_types, new_subtypes, rooted: make_rooted, parameters_type: parameters_type)
      end

      # @return [String]
      def rooted_tags
        rooted_tag
      end

      # @return [String]
      def tags
        tag
      end

      # @return [self]
      def force_rooted
        transform do |t|
          t.recreate(make_rooted: true)
        end
      end

      # Apply the given transformation to each subtype and then finally to this type
      #
      # @param new_name [String, nil]
      # @yieldparam t [UniqueType]
      # @yieldreturn [self]
      # @return [self]
      def transform(new_name = nil, &transform_type)
        raise "Please remove leading :: and set rooted with recreate() instead - #{new_name}" if new_name&.start_with?('::')
        if name == ComplexType::GENERIC_TAG_NAME
          # doesn't make sense to manipulate the name of the generic
          new_key_types = @key_types
          new_subtypes = @subtypes
        else
          new_key_types = @key_types.flat_map { |ct| ct.items.map { |ut| ut.transform(&transform_type) } }
          new_subtypes = @subtypes.flat_map { |ct| ct.items.map { |ut| ut.transform(&transform_type) } }
        end
        new_type = recreate(new_name: new_name || name, new_key_types: new_key_types, new_subtypes: new_subtypes, make_rooted: @rooted)
        yield new_type
      end

      # Generate a ComplexType that fully qualifies this type's namespaces.
      #
      # @param api_map [ApiMap] The ApiMap that performs qualification
      # @param context [String] The namespace from which to resolve names
      # @return [self, ComplexType, UniqueType] The generated ComplexType
      def qualify api_map, *gates
        transform do |t|
          next t if t.name == GENERIC_TAG_NAME
          next t if t.duck_type? || t.void? || t.undefined? || t.literal?
          open = t.rooted? ? [''] : gates
          fqns = api_map.qualify(t.non_literal_name, *open)
          if fqns.nil?
            next UniqueType::BOOLEAN if t.tag == 'Boolean'
            next UniqueType::UNDEFINED
          end
          t.recreate(new_name: fqns, make_rooted: true)
        end
      end

      def selfy?
        @name == 'self' || @key_types.any?(&:selfy?) || @subtypes.any?(&:selfy?)
      end

      # @param dst [ComplexType]
      # @return [self]
      def self_to_type dst
        object_type_dst = dst.reduce_class_type
        transform do |t|
          next t if t.name != 'self'
          object_type_dst
        end
      end

      def all_rooted?
        return true if name == GENERIC_TAG_NAME
        rooted? && all_params.all?(&:rooted?)
      end

      def rooted?
        !can_root_name? || @rooted
      end

      # @param name_to_check [String]
      def can_root_name?(name_to_check = name)
        self.class.can_root_name?(name_to_check)
      end

      # @param name [String]
      def self.can_root_name?(name)
        # name is not lowercase
        !name.empty? && name != name.downcase
      end

      UNDEFINED = UniqueType.new('undefined', rooted: false)
      BOOLEAN = UniqueType.new('Boolean', rooted: true)
      TRUE = UniqueType.new('true', rooted: true)
      FALSE = UniqueType.new('false', rooted: true)
      NIL = UniqueType.new('nil', rooted: true)
      # @type [Hash{String => UniqueType}]
      SINGLE_SUBTYPE = {
        '::TrueClass' => UniqueType::TRUE,
        '::FalseClass' => UniqueType::FALSE,
        '::NilClass' => UniqueType::NIL
      }.freeze


      include Logging
    end
  end
end
