# frozen_string_literal: true

module RBS
  module AST
    class TypeParam
      attr_reader :name, :variance, :location, :upper_bound_type, :lower_bound_type, :default_type

      def initialize(name:, variance:, upper_bound:, lower_bound:, location:, default_type: nil, unchecked: false)
        @name = name
        @variance = variance
        @upper_bound_type = upper_bound
        @lower_bound_type = lower_bound
        @location = location
        @default_type = default_type
        @unchecked = unchecked
      end

      def upper_bound
        case upper_bound_type
        when Types::ClassInstance, Types::ClassSingleton, Types::Interface
          upper_bound_type
        end
      end

      def lower_bound
        case lower_bound_type
        when Types::ClassInstance, Types::ClassSingleton, Types::Interface
          lower_bound_type
        end
      end

      def unchecked!(value = true)
        @unchecked = value ? true : false
        self
      end

      def unchecked?
        @unchecked
      end

      def ==(other)
        other.is_a?(TypeParam) &&
          other.name == name &&
          other.variance == variance &&
          other.upper_bound_type == upper_bound_type &&
          other.lower_bound_type == lower_bound_type &&
          other.default_type == default_type &&
          other.unchecked? == unchecked?
      end

      alias eql? ==

      def hash
        self.class.hash ^ name.hash ^ variance.hash ^ upper_bound_type.hash ^ lower_bound_type.hash ^ unchecked?.hash ^ default_type.hash
      end

      def to_json(state = JSON::State.new)
        {
          name: name,
          variance: variance,
          unchecked: unchecked?,
          location: location,
          upper_bound: upper_bound_type,
          lower_bound: lower_bound_type,
          default_type: default_type
        }.to_json(state)
      end

      def map_type(&block)
        if b = upper_bound_type
          _upper_bound_type = yield(b)
        end

        if b = lower_bound_type
          _lower_bound_type = yield(b)
        end

        if dt = default_type
          _default_type = yield(dt)
        end

        TypeParam.new(
          name: name,
          variance: variance,
          upper_bound: _upper_bound_type,
          lower_bound: _lower_bound_type,
          location: location,
          default_type: _default_type
        ).unchecked!(unchecked?)
      end

      def self.resolve_variables(params)
        return if params.empty?

        vars = Set.new(params.map(&:name))

        params.map! do |param|
          param.map_type {|bound| _ = subst_var(vars, bound) }
        end
      end

      def self.subst_var(vars, type)
        case type
        when Types::ClassInstance
          namespace = type.name.namespace
          if namespace.relative? && namespace.empty? && vars.member?(type.name.name)
            return Types::Variable.new(name: type.name.name, location: type.location)
          end
        end

        type.map_type {|t| subst_var(vars, t) }
      end

      def self.rename(params, new_names:)
        raise unless params.size == new_names.size

        subst = Substitution.build(new_names, Types::Variable.build(new_names))

        params.map.with_index do |param, index|
          new_name = new_names[index]

          TypeParam.new(
            name: new_name,
            variance: param.variance,
            upper_bound: param.upper_bound_type&.map_type {|type| type.sub(subst) },
            lower_bound: param.lower_bound_type&.map_type {|type| type.sub(subst) },
            location: param.location,
            default_type: param.default_type&.map_type {|type| type.sub(subst) }
          ).unchecked!(param.unchecked?)
        end
      end

      def to_s
        s = +""

        if unchecked?
          s << "unchecked "
        end

        case variance
        when :invariant
          # nop
        when :covariant
          s << "out "
        when :contravariant
          s << "in "
        end

        s << name.to_s

        if type = upper_bound_type
          s << " < #{type}"
        end

        if type = lower_bound_type
          s << " > #{type}"
        end

        if dt = default_type
          s << " = #{dt}"
        end

        s
      end

      def self.application(params, args)
        if params.empty?
          return nil
        end

        optional_params, required_params = params.partition {|param| param.default_type }

        param_subst = Substitution.new()
        app_subst = Substitution.new()

        required_params.zip(args.take(required_params.size)).each do |param, arg|
          arg ||= Types::Bases::Any.new(location: nil)
          param_subst.add(from: param.name, to: arg)
          app_subst.add(from: param.name, to: arg)
        end

        optional_params.each do |param|
          param_subst.add(from: param.name, to: Types::Bases::Any.new(location: nil))
        end

        optional_params.zip(args.drop(required_params.size)).each do |param, arg|
          if arg
            app_subst.add(from: param.name, to: arg)
          else
            param.default_type or raise
            app_subst.add(from: param.name, to: param.default_type.sub(param_subst))
          end
        end

        app_subst
      end

      def self.normalize_args(params, args)
        app = application(params, args) or return args

        min_count = params.count { _1.default_type.nil? }
        unless min_count <= args.size && args.size <= params.size
          return args
        end

        params.zip(args).filter_map do |param, arg|
          if arg
            arg
          else
            if param.default_type
              param.default_type.sub(app)
            else
              Types::Bases::Any.new(location: nil)
            end
          end
        end
      end

      def self.validate(type_params)
        optionals = type_params.filter {|param| param.default_type }

        optional_param_names = optionals.map(&:name).sort

        optionals.filter! do |param|
          default_type = param.default_type or raise
          optional_param_names.any? { default_type.free_variables.include?(_1) }
        end

        unless optionals.empty?
          optionals
        end
      end
    end
  end
end
