module TypeProf
  class Type # or AbstractValue
    include Utils::StructuralEquality

    def initialize
      raise "cannot instantiate abstract type"
    end

    Builtin = {}

    def globalize(_env, _visited, _depth)
      self
    end

    def localize(env, _alloc_site, _depth)
      return env, self
    end

    def limit_size(limit)
      self
    end

    def self.match?(ty1, ty2)
      # both ty1 and ty2 should be global
      # ty1 is always concrete; it should not have type variables
      # ty2 might be abstract; it may have type variables
      case ty2
      when Type::Var
        { ty2 => ty1 }
      when Type::Any
        {}
      when Type::Union
        subst = nil
        ty2.each_child_global do |ty2|
          # this is very conservative to create subst:
          # Type.match?( int | str, int | X) creates { X => int | str } but should be { X => str }???
          subst2 = Type.match?(ty1, ty2)
          next unless subst2
          subst = Type.merge_substitution(subst, subst2)
        end
        subst
      else
        case ty1
        when Type::Var then raise "should not occur"
        when Type::Any
          subst = {}
          ty2.each_free_type_variable do |tyvar|
            subst[tyvar] = Type.any
          end
          subst
        when Type::Union
          subst = nil
          ty1.each_child_global do |ty1|
            subst2 = Type.match?(ty1, ty2)
            next unless subst2
            subst = Type.merge_substitution(subst, subst2)
          end
          subst
        else
          if ty2.is_a?(Type::ContainerType)
            # ty2 may have type variables
            return nil if ty1.class != ty2.class
            ty1.match?(ty2)
          elsif ty1.is_a?(Type::ContainerType)
            nil
          else
            ty1.consistent?(ty2) ? {} : nil
          end
        end
      end
    end

    def self.merge_substitution(subst1, subst2)
      if subst1
        subst1 = subst1.dup
        subst2.each do |tyvar, ty|
          if subst1[tyvar]
            subst1[tyvar] = subst1[tyvar].union(ty)
          else
            subst1[tyvar] = ty
          end
        end
        subst1
      else
        subst2
      end
    end

    def each_child
      yield self
    end

    def each_child_global
      yield self
    end

    def each_free_type_variable
    end

    def union(other)
      return self if self == other # fastpath

      ty1, ty2 = self, other

      case
      when ty1.is_a?(Union)
        ty1_types = ty1.types
        ty1_elems = ty1.elems
      when ty1.is_a?(Array) || ty1.is_a?(Hash)
        ty1_types = Utils::Set[]
        ty1_elems = {[ty1.class, ty1.base_type] => ty1.elems}
      else
        ty1_types = ty1_elems = nil
      end

      case
      when ty2.is_a?(Union)
        ty2_types = ty2.types
        ty2_elems = ty2.elems
      when ty2.is_a?(Array) || ty2.is_a?(Hash)
        ty2_types = Utils::Set[]
        ty2_elems = {[ty2.class, ty2.base_type] => ty2.elems}
      else
        ty2_types = ty2_elems = nil
      end

      if ty1_types && ty2_types
        ty = ty1_types.sum(ty2_types)
        all_elems = ty1_elems.dup || {}
        ty2_elems&.each do |key, elems|
          all_elems[key] = union_elems(all_elems[key], elems)
        end
        all_elems = nil if all_elems.empty?

        Type::Union.create(ty, all_elems)
      elsif ty1_types
        Type::Union.create(ty1_types.add(ty2), ty1_elems)
      elsif ty2_types
        Type::Union.create(ty2_types.add(ty1), ty2_elems)
      else
        Type::Union.create(Utils::Set[ty1, ty2], nil)
      end
    end

    private def union_elems(e1, e2)
      if e1
        if e2
          e1.union(e2)
        else
          e1
        end
      else
        e2
      end
    end

    def substitute(_subst, _depth)
      raise "cannot substitute abstract type: #{ self.class }"
    end

    def generate_substitution
      {}
    end

    DummySubstitution = Object.new
    def DummySubstitution.[](_)
      Type.any
    end

    def remove_type_vars
      substitute(DummySubstitution, Config.current.options[:type_depth_limit])
    end

    def include_untyped?(_scratch)
      false
    end

    class Any < Type
      def initialize
      end

      def inspect
        "Type::Any"
      end

      def screen_name(scratch)
        "untyped"
      end

      def method_dispatch_info
        nil
      end

      def consistent?(_other)
        raise "should not be called"
      end

      def substitute(_subst, _depth)
        self
      end

      def include_untyped?(_scratch)
        true
      end
    end

    class Void < Any
      def inspect
        "Type::Void"
      end

      def screen_name(scratch)
        "void"
      end
    end


    class Union < Type
      def self.create(tys, elems)
        if tys.size == 1 && !elems
          tys.each {|ty| return ty }
        elsif tys.size == 0
          if elems && elems.size == 1
            (container_kind, base_type), nelems = elems.first
            # container_kind = Type::Array or Type::Hash
            container_kind.new(nelems, base_type)
          else
            new(tys, elems)
          end
        else
          class_instances = []
          non_class_instances = []
          degenerated = false
          tys.each do |ty|
            if ty != Type::Instance.new(Type::Builtin[:nil]) && ty.is_a?(Type::Instance) && ty.klass.kind == :class
              class_instances << ty
              degenerated = true if ty.include_subclasses
            else
              non_class_instances << ty
            end
          end
          if (Config.current.options[:union_width_limit] >= 2 && class_instances.size >= Config.current.options[:union_width_limit]) || (degenerated && class_instances.size >= 2)
            create(Utils::Set[Instance.new_degenerate(class_instances), *non_class_instances], elems)
          else
            new(tys, elems)
          end
        end
      end

      def initialize(tys, elems)
        raise unless tys.is_a?(Utils::Set)
        @types = tys # Set

        # invariant check
        local = nil
        tys.each do |ty|
          raise ty.inspect unless ty.is_a?(Type)
          local = true if ty.is_a?(Local)
        end
        raise if local && elems

        @elems = elems
        raise elems.inspect if elems && !elems.is_a?(::Hash)
      end

      def each_free_type_variable(&blk)
        each_child_global do |ty|
          ty.each_free_type_variable(&blk)
        end
      end

      def limit_size(limit)
        return Type.any if limit <= 0
        tys = Utils::Set[]
        @types.each do |ty|
          tys = tys.add(ty.limit_size(limit - 1))
        end
        elems = @elems&.to_h do |key, elems|
          [key, elems.limit_size(limit - 1)]
        end
        Union.new(tys, elems)
      end

      attr_reader :types, :elems

      def each_child(&blk) # local
        @types.each(&blk)
        raise if @elems
      end

      def each_child_global(&blk)
        @types.each(&blk)
        @elems&.each do |(container_kind, base_type), elems|
          yield container_kind.new(elems, base_type)
        end
      end

      def inspect
        a = []
        a << "Type::Union{#{ @types.to_a.map {|ty| ty.inspect }.join(", ") }"
        @elems&.each do |(container_kind, base_type), elems|
          a << ", #{ container_kind.new(elems, base_type).inspect }"
        end
        a << "}"
        a.join
      end

      def screen_name(scratch)
        types = @types.to_a
        @elems&.each do |(container_kind, base_type), elems|
          types << container_kind.new(elems, base_type)
        end
        if types.size == 0
          "bot"
        else
          types = types.to_a
          optional = !!types.delete(Type::Instance.new(Type::Builtin[:nil]))
          bool = false
          if types.include?(Type::Instance.new(Type::Builtin[:false])) &&
             types.include?(Type::Instance.new(Type::Builtin[:true]))
            types.delete(Type::Instance.new(Type::Builtin[:false]))
            types.delete(Type::Instance.new(Type::Builtin[:true]))
            bool = true
          end
          types.delete(Type.any) unless Config.current.options[:show_untyped]
          proc_tys, types = types.partition {|ty| ty.is_a?(Proc) }
          types = types.map {|ty| ty.screen_name(scratch) }
          types << scratch.show_proc_signature(proc_tys) unless proc_tys.empty?
          types << "bool" if bool
          types = types.sort
          if optional
            case types.size
            when 0 then "nil"
            when 1 then types.first + "?"
            else
              "(#{ types.join (" | ") })?"
            end
          else
            types.join (" | ")
          end
        end
      end

      def globalize(env, visited, depth)
        return Type.any if depth <= 0
        tys = Utils::Set[]
        if @elems
          # XXX: If @elems is non nil, the Union type should global, so calling globalize against such a type should not occur.
          # However, currently, ActualArguments may contain global types for flag_args_kw_splat case.
          # This should be fixed in future in ActualArguments side. See Scratch#setup_actual_arguments.
          #raise
        end

        elems = @elems ? @elems.dup : {}
        @types.each do |ty|
          ty = ty.globalize(env, visited, depth - 1)
          case ty
          when Type::Array, Type::Hash
            key = [ty.class, ty.base_type]
            elems[key] = union_elems(elems[key], ty.elems)
          else
            tys = tys.add(ty)
          end
        end
        elems = nil if elems.empty?

        Type::Union.create(tys, elems)
      end

      def localize(env, alloc_site, depth)
        return env, Type.any if depth <= 0
        tys = @types.map do |ty|
          env, ty2 = ty.localize(env, alloc_site, depth - 1)
          ty2
        end
        @elems&.each do |(container_kind, base_type), elems|
          ty = container_kind.new(elems, base_type)
          env, ty = ty.localize(env, alloc_site, depth - 1)
          tys = tys.add(ty)
        end
        ty = Union.create(tys, nil)
        return env, ty
      end

      def consistent?(_other)
        raise "should not be called"
      end

      def substitute(subst, depth)
        return Type.any if depth <= 0
        unions = []
        tys = Utils::Set[]
        @types.each do |ty|
          ty = ty.substitute(subst, depth - 1)
          case ty
          when Union
            unions << ty
          else
            tys = tys.add(ty)
          end
        end
        elems = @elems&.to_h do |(container_kind, base_type), elems|
          [[container_kind, base_type], elems.substitute(subst, depth - 1)]
        end
        ty = Union.create(tys, elems)
        unions.each do |ty0|
          ty = ty.union(ty0)
        end
        ty
      end

      def include_untyped?(scratch)
        @types.each do |ty|
          return true if ty.include_untyped?(scratch)
        end
        @elems&.each do |(container_kind, base_type), elems|
          return true if base_type.include_untyped?(scratch)
          return true if elems.include_untyped?(scratch)
        end
        false
      end
    end

    def self.any
      @any ||= Any.new
    end

    def self.bot
      @bot ||= Union.new(Utils::Set[], nil)
    end

    def self.bool
      @bool ||= Union.new(Utils::Set[
        Instance.new(Type::Builtin[:true]),
        Instance.new(Type::Builtin[:false])
      ], nil)
    end

    def self.nil
      @nil ||= Instance.new(Type::Builtin[:nil])
    end

    def self.optional(ty)
      ty.union(Type.nil)
    end

    class Var < Type
      def initialize(name)
        @name = name
      end

      def screen_name(scratch)
        "Var[#{ @name }]"
      end

      def each_free_type_variable
        yield self
      end

      def substitute(subst, depth)
        if subst[self]
          subst[self].limit_size(depth)
        else
          self
        end
      end

      def consistent?(_other)
        raise "should not be called: #{ self }"
      end

      def add_subst!(ty, subst)
        if subst[self]
          subst[self] = subst[self].union(ty)
        else
          subst[self] = ty
        end
        true
      end
    end

    class Class < Type # or Module
      def initialize(kind, idx, type_params, superclass, name)
        @kind = kind # :class | :module
        @idx = idx
        @type_params = type_params
        @superclass = superclass
        raise if @kind == :class && !@superclass
        @_name = name
      end

      attr_reader :kind, :idx, :type_params, :superclass
      attr_accessor :superclass_type_args

      def inspect
        if @_name
          "#{ @_name }@#{ @idx }"
        else
          "Class[#{ @idx }]"
        end
      end

      def screen_name(scratch)
        "singleton(#{ scratch.get_class_name(self) })"
      end

      def method_dispatch_info
        [self, true, false]
      end

      def consistent?(other)
        case other
        when Type::Class
          ty = self
          loop do
            # ad-hoc
            return false if !ty || !other # module

            return true if ty.idx == other.idx
            return false if ty.idx == 0 # Object
            ty = ty.superclass
          end
        when Type::Instance
          return true if other.klass == Type::Builtin[:obj] || other.klass == Type::Builtin[:class] || other.klass == Type::Builtin[:module]
          return false
        else
          false
        end
      end

      def substitute(_subst, _depth)
        self
      end
    end

    class Instance < Type
      def initialize(klass, include_subclasses=false)
        raise unless klass
        raise if klass == Type.any
        raise if klass.is_a?(Type::Instance)
        raise if klass.is_a?(Type::Union)
        @klass = klass
        @include_subclasses = include_subclasses
      end

      def self.new_degenerate(instances)
        klass = instances.first.klass
        ancestors = []
        ancestor_idxs = {}
        while klass != :__root__
          ancestor_idxs[klass] = ancestors.size
          ancestors << klass
          klass = klass.superclass
        end
        common_superclass = nil
        instances[1..].each do |instance|
          klass = instance.klass
          while !ancestor_idxs[klass]
            klass = klass.superclass
          end
          common_superclass = klass
          ancestor_idxs[klass].times do |i|
            ancestor_idxs.delete(ancestors[i])
            ancestors[i] = nil
          end
        end
        new(common_superclass, true)
      end

      attr_reader :klass, :include_subclasses

      def inspect
        "I[#{ @klass.inspect }]"
      end

      def screen_name(scratch)
        case @klass
        when Type::Builtin[:nil] then "nil"
        when Type::Builtin[:true] then "true"
        when Type::Builtin[:false] then "false"
        else
          scratch.get_class_name(@klass) + (@include_subclasses ? "" : "")
        end
      end

      def method_dispatch_info
        [@klass, false, @include_subclasses]
      end

      def consistent?(other)
        case other
        when Type::Instance
          @klass.consistent?(other.klass)
        when Type::Class
          return true if @klass == Type::Builtin[:obj] || @klass == Type::Builtin[:class] || @klass == Type::Builtin[:module]
          return false
        else
          false
        end
      end

      def substitute(subst, depth)
        Instance.new(@klass.substitute(subst, depth))
      end
    end

    # This is an internal object in MRI, so a user program cannot create this object explicitly
    class ISeq < Type
      def initialize(iseq)
        @iseq = iseq
      end

      attr_reader :iseq

      def inspect
        "Type::ISeq[#{ @iseq }]"
      end

      def screen_name(_scratch)
        raise NotImplementedError
      end
    end

    class Proc < Type
      def initialize(block_body, base_type)
        @block_body, @base_type = block_body, base_type
      end

      attr_reader :block_body, :base_type

      def consistent?(other)
        case other
        when Type::Proc
          @block_body.consistent?(other.block_body)
        else
          self == other
        end
      end

      def method_dispatch_info
        @base_type.method_dispatch_info
      end

      def substitute(subst, depth)
        Proc.new(@block_body.substitute(subst, depth), @base_type)
      end

      def screen_name(scratch)
        scratch.show_proc_signature([self])
      end

      def include_untyped?(scratch)
        false # XXX: need to check the block signatures recursively
      end
    end

    class Symbol < Type
      def initialize(sym, base_type)
        @sym = sym
        @base_type = base_type
      end

      attr_reader :sym, :base_type

      def inspect
        "Type::Symbol[#{ @sym ? @sym.inspect : "(dynamic symbol)" }, #{ @base_type.inspect }]"
      end

      def consistent?(other)
        case other
        when Symbol
          @sym == other.sym
        else
          @base_type.consistent?(other)
        end
      end

      def screen_name(scratch)
        if @sym
          @sym.inspect
        else
          @base_type.screen_name(scratch)
        end
      end

      def method_dispatch_info
        @base_type.method_dispatch_info
      end

      def substitute(_subst, _depth)
        self # dummy
      end
    end

    # A local type
    class Literal < Type
      def initialize(lit, base_type)
        @lit = lit
        @base_type = base_type
      end

      attr_reader :lit, :base_type

      def inspect
        "Type::Literal[#{ @lit.inspect }, #{ @base_type.inspect }]"
      end

      def screen_name(scratch)
        @base_type.screen_name(scratch) + "<#{ @lit.inspect }>"
      end

      def globalize(_env, _visited, _depth)
        @base_type
      end

      def method_dispatch_info
        @base_type.method_dispatch_info
      end

      def consistent?(_other)
        raise "should not called"
      end
    end

    class HashGenerator
      def initialize
        @map_tys = {}
      end

      attr_reader :map_tys

      def []=(k_ty, v_ty)
        k_ty.each_child_global do |k_ty|
          # This is a temporal hack to mitigate type explosion
          k_ty = Type.any if k_ty.is_a?(Type::Array)
          k_ty = Type.any if k_ty.is_a?(Type::Hash)

          if @map_tys[k_ty]
            @map_tys[k_ty] = @map_tys[k_ty].union(v_ty)
          else
            @map_tys[k_ty] = v_ty
          end
        end
      end
    end

    def self.gen_hash(base_ty = Type::Instance.new(Type::Builtin[:hash]))
      hg = HashGenerator.new
      yield hg
      Type::Hash.new(Type::Hash::Elements.new(hg.map_tys), base_ty)
    end

    def self.guess_literal_type(obj)
      case obj
      when ::Symbol
        Type::Symbol.new(obj, Type::Instance.new(Type::Builtin[:sym]))
      when ::Integer
        Type::Literal.new(obj, Type::Instance.new(Type::Builtin[:int]))
      when ::Rational
        Type::Literal.new(obj, Type::Instance.new(Type::Builtin[:rational]))
      when ::Complex
        Type::Literal.new(obj, Type::Instance.new(Type::Builtin[:complex]))
      when ::Float
        Type::Literal.new(obj, Type::Instance.new(Type::Builtin[:float]))
      when ::Class
        return Type.any if obj < Exception
        case obj
        when ::Object
          Type::Builtin[:obj]
        when ::Array
          Type::Builtin[:ary]
        else
          raise "unknown class: #{ obj.inspect }"
        end
      when ::TrueClass
        Type::Instance.new(Type::Builtin[:true])
      when ::FalseClass
        Type::Instance.new(Type::Builtin[:false])
      when ::Array
        base_ty = Type::Instance.new(Type::Builtin[:ary])
        lead_tys = obj.map {|arg| guess_literal_type(arg) }
        Type::Array.new(Type::Array::Elements.new(lead_tys), base_ty)
      when ::Hash
        Type.gen_hash do |h|
          obj.each do |k, v|
            k_ty = guess_literal_type(k).globalize(nil, {}, Config.current.options[:type_depth_limit])
            v_ty = guess_literal_type(v)
            h[k_ty] = v_ty
          end
        end
      when ::String
        Type::Literal.new(obj, Type::Instance.new(Type::Builtin[:str]))
      when ::Regexp
        Type::Literal.new(obj, Type::Instance.new(Type::Builtin[:regexp]))
      when ::NilClass
        Type.nil
      when ::Range
        Type::Literal.new(obj, Type::Instance.new(Type::Builtin[:range]))
      when ::Encoding
        Type::Literal.new(obj, Type::Instance.new(Type::Builtin[:encoding]))
      else
        raise "unknown object: #{ obj.inspect }"
      end
    end

    def self.builtin_global_variable_type(var)
      case var
      when :$_, :$/, :$\, :$,, :$;
        Type.optional(Type::Instance.new(Type::Builtin[:str]))
      when :$0, :$PROGRAM_NAME
        Type::Instance.new(Type::Builtin[:str])
      when :$~
        Type.optional(Type::Instance.new(Type::Builtin[:matchdata]))
      when :$., :$$
        Type::Instance.new(Type::Builtin[:int])
      when :$?
        Type.optional(Type::Instance.new(Type::Builtin[:int]))
      when :$!
        Type.optional(Type::Instance.new(Type::Builtin[:exc]))
      when :$@
        str = Type::Instance.new(Type::Builtin[:str])
        base_ty = Type::Instance.new(Type::Builtin[:ary])
        Type.optional(Type::Array.new(Type::Array::Elements.new([], str), base_ty))
      when :$*, :$:, :$LOAD_PATH, :$", :$LOADED_FEATURES
        str = Type::Instance.new(Type::Builtin[:str])
        base_ty = Type::Instance.new(Type::Builtin[:ary])
        Type::Array.new(Type::Array::Elements.new([], str), base_ty)
      when :$<
        :ARGF
      when :$>
        :STDOUT
      when :$DEBUG
        Type.bool
      when :$FILENAME
        Type::Instance.new(Type::Builtin[:str])
      when :$stdin
        :STDIN
      when :$stdout
        :STDOUT
      when :$stderr
        :STDERR
      when :$VERBOSE
        Type.bool.union(Type.nil)
      else
        nil
      end
    end
  end

  class Signature
    include Utils::StructuralEquality

    def screen_name(iseq, scratch)
      fargs_str = "("
      sig_help = {}
      add_farg = -> farg, name, help: false, key: sig_help.size do
        name = "`#{ name }`" if RBS::Parser::KEYWORDS.key?(name.to_s)
        name = "noname_#{ name }" if name.is_a?(Integer)
        fargs_str << ", " if fargs_str != "("
        i = fargs_str.size
        fargs_str << (Config.current.options[:show_parameter_names] && name ? "#{ farg } #{ name }" : farg)
        sig_help[key] = (i...fargs_str.size)
      end

      @lead_tys.zip(iseq ? iseq.locals : []) do |ty, name|
        add_farg.call(ty.screen_name(scratch), name, help: true)
      end

      @opt_tys&.zip(iseq ? iseq.locals[@lead_tys.size, @opt_tys.size] : []) do |ty, name|
        add_farg.call("?" + ty.screen_name(scratch), name, help: true)
      end

      if @rest_ty
        if iseq
          rest_index = iseq.fargs_format[:rest_start]
          name = rest_index ? iseq.locals[rest_index] : nil
        end
        add_farg.call("*" + @rest_ty.screen_name(scratch), name)
      end

      if iseq
        post_start = iseq.fargs_format[:post_start]
        names = post_start ? iseq.locals[post_start, @post_tys.size] : []
      end
      @post_tys&.zip(names || []) do |ty, name|
        add_farg.call(ty.screen_name(scratch), name)
      end

      @kw_tys&.each do |req, sym, ty|
        opt = req ? "" : "?"
        add_farg.call("#{ opt }#{ sym }: #{ ty.screen_name(scratch) }", nil, help: true, key: sym)
      end

      if @kw_rest_ty
        all_val_ty = Type.bot
        @kw_rest_ty.each_child_global do |ty|
          if ty == Type.any
            val_ty = ty
          else
            # ty is a Type::Hash
            _key_ty, val_ty = ty.elems.squash
          end
          all_val_ty = all_val_ty.union(val_ty)
        end
        add_farg.call("**" + all_val_ty.screen_name(scratch), nil)
      end

      fargs_str << ")"

      fargs_str = "" if fargs_str == "()"

      # Dirty Hack: Stop the iteration at most once!
      # I'll remove this hack if RBS removes the limitation of nesting blocks
      return fargs_str, sig_help if caller_locations.any? {|frame| frame.label == "show_block_signature" }

      optional = false
      blks = []
      @blk_ty.each_child_global do |ty|
        if ty.is_a?(Type::Proc)
          blks << ty
        else
          # XXX: how should we handle types other than Type.nil
          optional = true
        end
      end
      if blks != []
        fargs_str << " " if fargs_str != ""
        fargs_str << "?" if optional
        fargs_str << scratch.show_block_signature(blks)
      end

      return fargs_str, sig_help
    end
  end

  class MethodSignature < Signature
    def initialize(lead_tys, opt_tys, rest_ty, post_tys, kw_tys, kw_rest_ty, blk_ty)
      @lead_tys = lead_tys
      @opt_tys = opt_tys
      raise unless opt_tys.is_a?(Array)
      @rest_ty = rest_ty
      @post_tys = post_tys
      raise unless post_tys
      @kw_tys = kw_tys
      kw_tys.each {|a| raise if a.size != 3 } if kw_tys
      @kw_rest_ty = kw_rest_ty
      kw_rest_ty&.each_child_global do |ty|
        raise ty.inspect if ty != Type.any && !ty.is_a?(Type::Hash)
      end
      @blk_ty = blk_ty
    end

    def include_untyped?(scratch)
      return true if @lead_tys.any? {|ty| ty.include_untyped?(scratch) }
      return true if @opt_tys.any? {|ty| ty.include_untyped?(scratch) }
      return true if @rest_ty&.include_untyped?(scratch)
      return true if @post_tys.any? {|ty| ty.include_untyped?(scratch) }
      return true if @kw_tys&.any? {|_, _, ty| ty.include_untyped?(scratch) }
      return true if @kw_rest_ty&.include_untyped?(scratch)
      return true if @blk_ty&.include_untyped?(scratch)
      false
    end

    attr_reader :lead_tys, :opt_tys, :rest_ty, :post_tys, :kw_tys, :kw_rest_ty, :blk_ty

    def substitute(subst, depth)
      lead_tys = @lead_tys.map {|ty| ty.substitute(subst, depth - 1) }
      opt_tys = @opt_tys.map {|ty| ty.substitute(subst, depth - 1) }
      rest_ty = @rest_ty&.substitute(subst, depth - 1)
      post_tys = @post_tys.map {|ty| ty.substitute(subst, depth - 1) }
      kw_tys = @kw_tys.map {|req, key, ty| [req, key, ty.substitute(subst, depth - 1)] }
      kw_rest_ty = @kw_rest_ty&.substitute(subst, depth - 1)
      blk_ty = @blk_ty.substitute(subst, depth - 1)
      MethodSignature.new(lead_tys, opt_tys, rest_ty, post_tys, kw_tys, kw_rest_ty, blk_ty)
    end

    def merge_as_block_arguments(other)
      lead_tys1, opt_tys1, rest_ty1, post_tys1 = @lead_tys, @opt_tys, @rest_ty, @post_tys
      lead_tys2, opt_tys2, rest_ty2, post_tys2 = other.lead_tys, other.opt_tys, other.rest_ty, other.post_tys

      case
      when lead_tys1.size > lead_tys2.size
        n = lead_tys2.size
        lead_tys1, opt_tys1 = lead_tys1[0, n], lead_tys1[n..] + opt_tys1
      when lead_tys1.size < lead_tys2.size
        n = lead_tys1.size
        lead_tys2, opt_tys2 = lead_tys2[0, n], lead_tys2[n..] + opt_tys2
      end
      case
      when post_tys1.size > post_tys2.size
        i = post_tys1.size - post_tys2.size
        if rest_ty1
          rest_ty1 = post_tys[0, i].inject(rest_ty1) {|ty1, ty2| ty1.union(ty2) }
          post_tys1 = post_tys1[i..]
        else
          opt_tys1, post_tys1 = opt_tys1 + post_tys1[0, i], post_tys1[i..]
        end
      when post_tys1.size < post_tys2.size
        i = post_tys2.size - post_tys1.size
        if rest_ty2
          rest_ty2 = post_tys[0, i].inject(rest_ty2) {|ty1, ty2| ty1.union(ty2) }
          post_tys2 = post_tys2[i..]
        else
          opt_tys2, post_tys2 = opt_tys2 + post_tys2[0, i], post_tys2[i..]
        end
      end

      # XXX: tweak keywords too

      msig1 = MethodSignature.new(lead_tys1, opt_tys1, rest_ty1, post_tys1, @kw_tys, @kw_rest_ty, @blk_ty)
      msig2 = MethodSignature.new(lead_tys2, opt_tys2, rest_ty2, post_tys2, other.kw_tys, other.kw_rest_ty, other.blk_ty)
      msig1.merge(msig2)
    end

    def merge(other)
      raise if @lead_tys.size != other.lead_tys.size
      raise if @post_tys.size != other.post_tys.size
      if @kw_tys && other.kw_tys
        kws1 = {}
        @kw_tys.each {|req, kw, _| kws1[kw] = req }
        kws2 = {}
        other.kw_tys.each {|req, kw, _| kws2[kw] = req }
        (kws1.keys & kws2.keys).each do |kw|
          raise if !!kws1[kw] != !!kws2[kw]
        end
      elsif @kw_tys || other.kw_tys
        (@kw_tys || other.kw_tys).each do |req,|
          raise if req
        end
      end
      lead_tys = @lead_tys.zip(other.lead_tys).map {|ty1, ty2| ty1.union(ty2) }
      if @opt_tys || other.opt_tys
        opt_tys = []
        [@opt_tys.size, other.opt_tys.size].max.times do |i|
          ty1 = @opt_tys[i]
          ty2 = other.opt_tys[i]
          ty = ty1 ? ty2 ? ty1.union(ty2) : ty1 : ty2
          opt_tys << ty
        end
      end
      if @rest_ty || other.rest_ty
        if @rest_ty && other.rest_ty
          rest_ty = @rest_ty.union(other.rest_ty)
        else
          rest_ty = @rest_ty || other.rest_ty
        end
      end
      post_tys = @post_tys.zip(other.post_tys).map {|ty1, ty2| ty1.union(ty2) }
      if @kw_tys && other.kw_tys
        kws1 = {}
        @kw_tys.each {|req, kw, ty| kws1[kw] = [req, ty] }
        kws2 = {}
        other.kw_tys.each {|req, kw, ty| kws2[kw] = [req, ty] }
        kw_tys = (kws1.keys | kws2.keys).map do |kw|
          req1, ty1 = kws1[kw]
          _req2, ty2 = kws2[kw]
          ty1 ||= Type.bot
          ty2 ||= Type.bot
          [!!req1, kw, ty1.union(ty2)]
        end
      elsif @kw_tys || other.kw_tys
        kw_tys = @kw_tys || other.kw_tys
      else
        kw_tys = nil
      end
      if @kw_rest_ty || other.kw_rest_ty
        if @kw_rest_ty && other.kw_rest_ty
          kw_rest_ty = @kw_rest_ty.union(other.kw_rest_ty)
        else
          kw_rest_ty = @kw_rest_ty || other.kw_rest_ty
        end
      end
      blk_ty = @blk_ty.union(other.blk_ty) if @blk_ty
      MethodSignature.new(lead_tys, opt_tys, rest_ty, post_tys, kw_tys, kw_rest_ty, blk_ty)
    end
  end

  class BlockSignature < Signature
    def initialize(lead_tys, opt_tys, rest_ty, blk_ty)
      @lead_tys = lead_tys
      @opt_tys = opt_tys
      @rest_ty = rest_ty
      @blk_ty = blk_ty
      # TODO: kw_tys
    end

    attr_reader :lead_tys, :opt_tys, :rest_ty, :blk_ty

    def merge(bsig)
      if @rest_ty && bsig.rest_ty
        rest_ty = @rest_ty.union(bsig.rest_ty)
        BlockSignature.new(@lead_tys, [], rest_ty, @blk_ty.union(bsig.blk_ty))
      elsif @rest_ty || bsig.rest_ty
        rest_ty = @rest_ty || bsig.rest_ty
        rest_ty = @opt_tys.inject(rest_ty, &:union)
        rest_ty = bsig.opt_tys.inject(rest_ty, &:union)

        lead_tys = []
        [@lead_tys.size, bsig.lead_tys.size].max.times do |i|
          ty1 = @lead_tys[i]
          ty2 = bsig.lead_tys[i]
          if ty1 && ty2
            lead_tys << ty1.union(ty2)
          else
            rest_ty = rest_ty.union(ty1 || ty2)
          end
        end

        BlockSignature.new(lead_tys, [], rest_ty, @blk_ty.union(bsig.blk_ty))
      else
        lead_tys = []
        n = [@lead_tys.size, bsig.lead_tys.size].min
        n.times do |i|
          lead_tys << @lead_tys[i].union(bsig.lead_tys[i])
        end
        opt_tys1 = @lead_tys[n..] + @opt_tys
        opt_tys2 = bsig.lead_tys[n..] + bsig.opt_tys
        opt_tys = []
        [opt_tys1.size, opt_tys2.size].max.times do |i|
          if opt_tys1[i] && opt_tys2[i]
            opt_tys << opt_tys1[i].union(opt_tys2[i])
          else
            opt_tys << (opt_tys1[i] || opt_tys2[i])
          end
        end
        BlockSignature.new(lead_tys, opt_tys, nil, @blk_ty.union(bsig.blk_ty))
      end
    end
  end
end
