# frozen_string_literal: true

module Solargraph
  # A container for type data based on YARD type tags.
  #
  class ComplexType
    GENERIC_TAG_NAME = 'generic'.freeze
    # @!parse
    #   include TypeMethods
    include Equality

    autoload :TypeMethods, 'solargraph/complex_type/type_methods'
    autoload :UniqueType,  'solargraph/complex_type/unique_type'

    # @param types [Array<UniqueType, ComplexType>]
    def initialize types = [UniqueType::UNDEFINED]
      # @todo @items here should not need an annotation
      # @type [Array<UniqueType>]
      items = types.flat_map(&:items).uniq(&:to_s)
      if items.any? { |i| i.name == 'false' } && items.any? { |i| i.name == 'true' }
        items.delete_if { |i| i.name == 'false' || i.name == 'true' }
        items.unshift(ComplexType::BOOLEAN)
      end
      items = [UniqueType::UNDEFINED] if items.any?(&:undefined?)
      @items = items
    end

    # @sg-ignore Fix "Not enough arguments to Module#protected"
    protected def equality_fields
      [self.class, items]
    end

    # @param api_map [ApiMap]
    # @param context [String]
    # @return [ComplexType]
    def qualify api_map, *gates
      red = reduce_object
      types = red.items.map do |t|
        next t if ['nil', 'void', 'undefined'].include?(t.name)
        next t if ['::Boolean'].include?(t.rooted_name)
        t.qualify api_map, *gates
      end
      ComplexType.new(types).reduce_object
    end

    # @param generics_to_resolve [Enumerable<String>]]
    # @param context_type [UniqueType, nil]
    # @param resolved_generic_values [Hash{String => ComplexType}] Added to as types are encountered or resolved
    # @return [self]
    def resolve_generics_from_context generics_to_resolve, context_type, resolved_generic_values: {}
      return self unless generic?

      ComplexType.new(@items.map { |i| i.resolve_generics_from_context(generics_to_resolve, context_type, resolved_generic_values: resolved_generic_values) })
    end

    # @return [UniqueType]
    def first
      @items.first
    end

    # @return [String]
    def to_rbs
      ((@items.length > 1 ? '(' : '') +
       @items.map(&:to_rbs).join(' | ') +
       (@items.length > 1 ? ')' : ''))
    end

    # @param dst [ComplexType]
    # @return [ComplexType]
    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

    # @yieldparam [UniqueType]
    # @return [Array<UniqueType>]
    def map &block
      @items.map &block
    end

    # @yieldparam [UniqueType]
    # @return [Enumerable<UniqueType>]
    def each &block
      @items.each &block
    end

    # @yieldparam [UniqueType]
    # @return [void]
    # @overload each_unique_type()
    #   @return [Enumerator<UniqueType>]
    def each_unique_type &block
      return enum_for(__method__) unless block_given?

      @items.each do |item|
        item.each_unique_type &block
      end
    end

    # @param atype [ComplexType] type which may be assigned to this type
    # @param api_map [ApiMap] The ApiMap that performs qualification
    def can_assign?(api_map, atype)
      any? { |ut| ut.can_assign?(api_map, atype) }
    end

    # @return [Integer]
    def length
      @items.length
    end

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

    # @param index [Integer]
    # @return [UniqueType]
    def [](index)
      @items[index]
    end

    # @return [Array<UniqueType>]
    def select &block
      @items.select &block
    end

    # @return [String]
    def namespace
      # cache this attr for high frequency call
      @namespace ||= method_missing(:namespace).to_s
    end

    # @return [Array<String>]
    def namespaces
      @items.map(&:namespace)
    end

    # @param name [Symbol]
    # @return [Object, nil]
    def method_missing name, *args, &block
      return if @items.first.nil?
      return @items.first.send(name, *args, &block) if respond_to_missing?(name)
      super
    end

    # @param name [Symbol]
    # @param include_private [Boolean]
    def respond_to_missing?(name, include_private = false)
      TypeMethods.public_instance_methods.include?(name) || super
    end

    def to_s
      map(&:tag).join(', ')
    end

    # @return [String]
    def tags
      map(&:tag).join(', ')
    end

    # @return [String]
    def simple_tags
      simplify_literals.tags
    end

    def literal?
      @items.any?(&:literal?)
    end

    # @return [ComplexType]
    def downcast_to_literal_if_possible
      ComplexType.new(items.map(&:downcast_to_literal_if_possible))
    end

    # @return [String]
    def desc
      rooted_tags
    end

    # @return [String]
    def rooted_tags
      map(&:rooted_tag).join(', ')
    end

    # @yieldparam [UniqueType]
    def all? &block
      @items.all? &block
    end

    # @yieldparam [UniqueType]
    # @yieldreturn [Boolean]
    # @return [Boolean]
    def any? &block
      @items.compact.any? &block
    end

    def selfy?
      @items.any?(&:selfy?)
    end

    def generic?
      any?(&:generic?)
    end

    # @return [self]
    def simplify_literals
      ComplexType.new(map(&:simplify_literals))
    end

    # @param new_name [String, nil]
    # @yieldparam t [UniqueType]
    # @yieldreturn [UniqueType]
    # @return [ComplexType]
    def transform(new_name = nil, &transform_type)
      raise "Please remove leading :: and set rooted with recreate() instead - #{new_name}" if new_name&.start_with?('::')
      ComplexType.new(map { |ut| ut.transform(new_name, &transform_type) })
    end

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

    # @param definitions [Pin::Namespace, Pin::Method]
    # @param context_type [ComplexType]
    # @return [ComplexType]
    def resolve_generics definitions, context_type
      result = @items.map { |i| i.resolve_generics(definitions, context_type) }
      ComplexType.new(result)
    end

    def nullable?
      @items.any?(&:nil_type?)
    end

    # @return [Array<ComplexType>]
    def all_params
      @items.first.all_params || []
    end

    # @return [ComplexType]
    def reduce_class_type
      new_items = items.flat_map do |type|
        next type unless ['Module', 'Class'].include?(type.name)
        next type if type.all_params.empty?

        type.all_params
      end
      ComplexType.new(new_items)
    end

    # every type and subtype in this union have been resolved to be
    # fully qualified
    def all_rooted?
      all?(&:all_rooted?)
    end

    # every top-level type has resolved to be fully qualified; see
    # #all_rooted? to check their subtypes as well
    def rooted?
      all?(&:rooted?)
    end

    attr_reader :items

    def rooted?
      @items.all?(&:rooted?)
    end

    protected

    # @return [ComplexType]
    def reduce_object
      new_items = items.flat_map do |ut|
        next [ut] if ut.name != 'Object' || ut.subtypes.empty?
        ut.subtypes
      end
      ComplexType.new(new_items)
    end

    def bottom?
      @items.all?(&:bot?)
    end

    class << self
      # Parse type strings into a ComplexType.
      #
      # @example
      #   ComplexType.parse 'String', 'Foo', 'nil' #=> [String, Foo, nil]
      #
      # @note
      #   The `partial` parameter is used to indicate that the method is
      #   receiving a string that will be used inside another ComplexType.
      #   It returns arrays of ComplexTypes instead of a single cohesive one.
      #   Consumers should not need to use this parameter; it should only be
      #   used internally.
      #
      # @param strings [Array<String>] The type definitions to parse
      # @return [ComplexType]
      # # @overload parse(*strings, partial: false)
      # #  @todo Need ability to use a literal true as a type below
      # #  @param partial [Boolean] True if the string is part of a another type
      # #  @return [Array<UniqueType>]
      # @todo To be able to select the right signature above,
      #   Chain::Call needs to know the decl type (:arg, :optarg,
      #   :kwarg, etc) of the arguments given, instead of just having
      #   an array of Chains as the arguments.
      def parse *strings, partial: false
        # @type [Hash{Array<String> => ComplexType}]
        @cache ||= {}
        unless partial
          cached = @cache[strings]
          return cached unless cached.nil?
        end
        types = []
        key_types = nil
        strings.each do |type_string|
          point_stack = 0
          curly_stack = 0
          paren_stack = 0
          base = String.new
          subtype_string = String.new
          type_string&.each_char do |char|
            if char == '='
              #raise ComplexTypeError, "Invalid = in type #{type_string}" unless curly_stack > 0
            elsif char == '<'
              point_stack += 1
            elsif char == '>'
              if subtype_string.end_with?('=') && curly_stack > 0
                subtype_string += char
              elsif base.end_with?('=')
                raise ComplexTypeError, "Invalid hash thing" unless key_types.nil?
                # types.push ComplexType.new([UniqueType.new(base[0..-2].strip)])
                types.push UniqueType.parse(base[0..-2].strip, subtype_string)
                # @todo this should either expand key_type's type
                #   automatically or complain about not being
                #   compatible with key_type's type in type checking
                key_types = types
                types = []
                base.clear
                subtype_string.clear
                next
              else
                raise ComplexTypeError, "Invalid close in type #{type_string}" if point_stack == 0
                point_stack -= 1
                subtype_string += char
              end
              next
            elsif char == '{'
              curly_stack += 1
            elsif char == '}'
              curly_stack -= 1
              subtype_string += char
              raise ComplexTypeError, "Invalid close in type #{type_string}" if curly_stack < 0
              next
            elsif char == '('
              paren_stack += 1
            elsif char == ')'
              paren_stack -= 1
              subtype_string += char
              raise ComplexTypeError, "Invalid close in type #{type_string}" if paren_stack < 0
              next
            elsif char == ',' && point_stack == 0 && curly_stack == 0 && paren_stack == 0
              # types.push ComplexType.new([UniqueType.new(base.strip, subtype_string.strip)])
              types.push UniqueType.parse(base.strip, subtype_string.strip)
              base.clear
              subtype_string.clear
              next
            end
            if point_stack == 0 && curly_stack == 0 && paren_stack == 0
              base.concat char
            else
              subtype_string.concat char
            end
          end
          raise ComplexTypeError, "Unclosed subtype in #{type_string}" if point_stack != 0 || curly_stack != 0 || paren_stack != 0
          # types.push ComplexType.new([UniqueType.new(base, subtype_string)])
          types.push UniqueType.parse(base.strip, subtype_string.strip)
        end
        unless key_types.nil?
          raise ComplexTypeError, "Invalid use of key/value parameters" unless partial
          return key_types if types.empty?
          return [key_types, types]
        end
        result = partial ? types : ComplexType.new(types)
        @cache[strings] = result unless partial
        result
      end

      # @param strings [Array<String>]
      # @return [ComplexType]
      def try_parse *strings
        parse *strings
      rescue ComplexTypeError => e
        Solargraph.logger.info "Error parsing complex type `#{strings.join(', ')}`: #{e.message}"
        ComplexType::UNDEFINED
      end
    end

    VOID = ComplexType.parse('void')
    UNDEFINED = ComplexType.parse('undefined')
    SYMBOL = ComplexType.parse('::Symbol')
    ROOT = ComplexType.parse('::Class<>')
    NIL = ComplexType.parse('nil')
    SELF = ComplexType.parse('self')
    BOOLEAN = ComplexType.parse('::Boolean')
    BOT = ComplexType.parse('bot')

    private

    # @todo This is a quick and dirty hack that forces `self` keywords
    #   to reference an instance of their class and never the class itself.
    #   This behavior may change depending on which result is expected
    #   from YARD conventions. See https://github.com/lsegal/yard/issues/1257
    # @param dst [String]
    # @return [String]
    def reduce_class dst
      while dst =~ /^(Class|Module)\<(.*?)\>$/
        dst = dst.sub(/^(Class|Module)\</, '').sub(/\>$/, '')
      end
      dst
    end
  end
end
