# frozen_string_literal: true

require_relative 'iterable'
require_relative 'recursion_guard'
require_relative 'type_acceptor'
require_relative 'type_asserter'
require_relative 'type_assertion_error'
require_relative 'type_conversion_error'
require_relative 'type_formatter'
require_relative 'type_calculator'
require_relative 'type_factory'
require_relative 'type_parser'
require_relative 'class_loader'
require_relative 'type_mismatch_describer'
require_relative 'puppet_object'

module Puppet::Pops
module Types
# The EMPTY_xxx declarations is for backward compatibility. They should not be explicitly referenced

# @api private
# @deprecated
EMPTY_HASH = Puppet::Pops::EMPTY_HASH

# @api private
# @deprecated
EMPTY_ARRAY = Puppet::Pops::EMPTY_ARRAY

# @api private
# @deprecated
EMPTY_STRING = Puppet::Pops::EMPTY_STRING

# The Types model is a model of Puppet Language types.
#
# The {TypeCalculator} should be used to answer questions about types. The {TypeFactory} or {TypeParser} should be used
# to create an instance of a type whenever one is needed.
#
# The implementation of the Types model contains methods that are required for the type objects to behave as
# expected when comparing them and using them as keys in hashes. (No other logic is, or should be included directly in
# the model's classes).
#
# @api public
#
class TypedModelObject < Object
  include PuppetObject
  include Visitable
  include Adaptable

  def self._pcore_type
    @type
  end

  def self.create_ptype(loader, ir, parent_name, attributes_hash = EMPTY_HASH)
    @type = Pcore.create_object_type(loader, ir, self, "Pcore::#{simple_name}Type", "Pcore::#{parent_name}", attributes_hash)
  end

  def self.register_ptypes(loader, ir)
    types = [
      Annotation.register_ptype(loader, ir),
      RubyMethod.register_ptype(loader, ir)
    ]
    Types.constants.each do |c|
      next if c == :PType || c == :PHostClassType

      cls = Types.const_get(c)
      next unless cls.is_a?(Class) && cls < self

      type = cls.register_ptype(loader, ir)
      types << type unless type.nil?
    end
    types.each { |type| type.resolve(loader) }
  end
end

# Base type for all types
# @api public
#
class PAnyType < TypedModelObject
  def self.register_ptype(loader, ir)
    @type = Pcore.create_object_type(loader, ir, self, 'Pcore::AnyType', 'Any', EMPTY_HASH)
  end

  def self.create(*args)
    # NOTE! Important to use self::DEFAULT and not just DEFAULT since the latter yields PAnyType::DEFAULT
    args.empty? ? self::DEFAULT : new(*args)
  end

  # Accept a visitor that will be sent the message `visit`, once with `self` as the
  # argument. The visitor will then visit all types that this type contains.
  #
  def accept(visitor, guard)
    visitor.visit(self, guard)
  end

  # Checks if _o_ is a type that is assignable to this type.
  # If _o_ is a `Class` then it is first converted to a type.
  # If _o_ is a Variant, then it is considered assignable when all its types are assignable
  #
  # The check for assignable must be guarded against self recursion since `self`, the given type _o_,
  # or both, might be a `TypeAlias`. The initial caller of this method will typically never care
  # about this and hence pass only the first argument, but as soon as a check of a contained type
  # encounters a `TypeAlias`, then a `RecursionGuard` instance is created and passed on in all
  # subsequent calls. The recursion is allowed to continue until self recursion has been detected in
  # both `self` and in the given type. At that point the given type is considered to be assignable
  # to `self` since all checks up to that point were positive.
  #
  # @param o [Class,PAnyType] the class or type to test
  # @param guard [RecursionGuard] guard against recursion. Only used by internal calls
  # @return [Boolean] `true` when _o_ is assignable to this type
  # @api public
  def assignable?(o, guard = nil)
    case o
    when Class
      # Safe to call _assignable directly since a Class never is a Unit or Variant
      _assignable?(TypeCalculator.singleton.type(o), guard)
    when PUnitType
      true
    when PTypeAliasType
      # An alias may contain self recursive constructs.
      if o.self_recursion?
        guard ||= RecursionGuard.new
        if guard.add_that(o) == RecursionGuard::SELF_RECURSION_IN_BOTH
          # Recursion detected both in self and other. This means that other is assignable
          # to self. This point would not have been reached otherwise
          true
        else
          assignable?(o.resolved_type, guard)
        end
      else
        assignable?(o.resolved_type, guard)
      end
    when PVariantType
      # Assignable if all contained types are assignable, or if this is exactly Any
      return true if instance_of?(PAnyType)
      # An empty variant may be assignable to NotUndef[T] if T is assignable to empty variant
      return _assignable?(o, guard) if is_a?(PNotUndefType) && o.types.empty?

      !o.types.empty? && o.types.all? { |vt| assignable?(vt, guard) }
    when POptionalType
      # Assignable if undef and contained type is assignable
      assignable?(PUndefType::DEFAULT) && (o.type.nil? || assignable?(o.type))
    when PNotUndefType
      if !(o.type.nil? || o.type.assignable?(PUndefType::DEFAULT))
        assignable?(o.type, guard)
      else
        _assignable?(o, guard)
      end
    else
      _assignable?(o, guard)
    end
  end

  # Returns `true` if this instance is a callable that accepts the given _args_type_ type
  #
  # @param args_type [PAnyType] the arguments to test
  # @param guard [RecursionGuard] guard against recursion. Only used by internal calls
  # @return [Boolean] `true` if this instance is a callable that accepts the given _args_
  def callable?(args_type, guard = nil)
    args_type.is_a?(PAnyType) && kind_of_callable? && args_type.callable_args?(self, guard)
  end

  # Returns `true` if this instance is a callable that accepts the given _args_
  #
  # @param args [Array] the arguments to test
  # @param block [Proc] block, or nil if not called with a block
  # @return [Boolean] `true` if this instance is a callable that accepts the given _args_
  def callable_with?(args, block = nil)
    false
  end

  # Returns `true` if this instance is considered valid as arguments to the given `callable`
  # @param callable [PAnyType] the callable
  # @param guard [RecursionGuard] guard against recursion. Only used by internal calls
  # @return [Boolean] `true` if this instance is considered valid as arguments to the given `callable`
  # @api private
  def callable_args?(callable, guard)
    false
  end

  # Called from the `PTypeAliasType` when it detects self recursion. The default is to do nothing
  # but some self recursive constructs are illegal such as when a `PObjectType` somehow inherits itself
  # @param originator [PTypeAliasType] the starting point for the check
  # @raise Puppet::Error if an illegal self recursion is detected
  # @api private
  def check_self_recursion(originator)
  end

  # Generalizes value specific types. Types that are not value specific will return `self` otherwise
  # the generalized type is returned.
  #
  # @return [PAnyType] The generalized type
  # @api public
  def generalize
    # Applicable to all types that have no variables
    self
  end

  # Returns the loader that loaded this type.
  # @return [Loaders::Loader] the loader
  def loader
    Loaders.static_loader
  end

  # Normalizes the type. This does not change the characteristics of the type but it will remove duplicates
  # and constructs like NotUndef[T] where T is not assignable from Undef and change Variant[*T] where all
  # T are enums into an Enum.
  #
  # @param guard [RecursionGuard] guard against recursion. Only used by internal calls
  # @return [PAnyType] The iterable type that this type is assignable to or `nil`
  # @api public
  def normalize(guard = nil)
    self
  end

  # Called from the TypeParser once it has found a type using the Loader to enable that this type can
  # resolve internal type expressions using a loader. Presently, this method is a no-op for all types
  # except the {{PTypeAliasType}}.
  #
  # @param loader [Loader::Loader] loader to use
  # @return [PTypeAliasType] the receiver of the call, i.e. `self`
  # @api private
  def resolve(loader)
    self
  end

  # Responds `true` for all callables, variants of callables and unless _optional_ is
  # false, all optional callables.
  # @param optional [Boolean]
  # @param guard [RecursionGuard] guard against recursion. Only used by internal calls
  # @return [Boolean] `true`if this type is considered callable
  # @api private
  def kind_of_callable?(optional = true, guard = nil)
    false
  end

  # Returns `true` if an instance of this type is iterable, `false` otherwise
  # The method #iterable_type must produce a `PIterableType` instance when this
  # method returns `true`
  #
  # @param guard [RecursionGuard] guard against recursion. Only used by internal calls
  # @return [Boolean] flag to indicate if instances of  this type is iterable.
  def iterable?(guard = nil)
    false
  end

  # Returns the `PIterableType` that this type should be assignable to, or `nil` if no such type exists.
  # A type that returns a `PIterableType` must respond `true` to `#iterable?`.
  #
  # @example
  #     Any Collection[T] is assignable to an Iterable[T]
  #     A String is assignable to an Iterable[String] iterating over the strings characters
  #     An Integer is assignable to an Iterable[Integer] iterating over the 'times' enumerator
  #     A Type[T] is assignable to an Iterable[Type[T]] if T is an Integer or Enum
  #
  # @param guard [RecursionGuard] guard against recursion. Only used by internal calls
  # @return [PIterableType,nil] The iterable type that this type is assignable to or `nil`
  # @api private
  def iterable_type(guard = nil)
    nil
  end

  def hash
    self.class.hash
  end

  # Returns true if the given argument _o_ is an instance of this type
  # @param guard [RecursionGuard] guard against recursion. Only used by internal calls
  # @return [Boolean]
  # @api public
  def instance?(o, guard = nil)
    true
  end

  # An object is considered to really be an instance of a type when something other than a
  # TypeAlias or a Variant responds true to a call to {#instance?}.
  #
  # @return [Integer] -1 = is not instance, 0 = recursion detected, 1 = is instance
  # @api private
  def really_instance?(o, guard = nil)
    instance?(o, guard) ? 1 : -1
  end

  def eql?(o)
    self.class == o.class
  end

  def ==(o)
    eql?(o)
  end

  def simple_name
    self.class.simple_name
  end

  # Strips the class name from all module prefixes, the leading 'P' and the ending 'Type'. I.e.
  # an instance of PVariantType will return 'Variant'
  # @return [String] the simple name of this type
  def self.simple_name
    @simple_name ||= (
      n = name
      n[n.rindex(DOUBLE_COLON) + 3..n.size - 5].freeze
    )
  end

  def to_alias_expanded_s
    TypeFormatter.new.alias_expanded_string(self)
  end

  def to_s
    TypeFormatter.string(self)
  end

  # Returns the name of the type, without parameters
  # @return [String] the name of the type
  # @api public
  def name
    simple_name
  end

  def create(*args)
    Loaders.find_loader(nil).load(:function, 'new').call({}, self, *args)
  end

  # Create an instance of this type.
  # The default implementation will just dispatch the call to the class method with the
  # same name and pass `self` as the first argument.
  #
  # @return [Function] the created function
  # @raises ArgumentError
  #
  def new_function
    self.class.new_function(self)
  end

  # This default implementation of of a new_function raises an Argument Error.
  # Types for which creating a new instance is supported, should create and return
  # a Puppet Function class by using Puppet:Loaders.create_loaded_function(:new, loader)
  # and return that result.
  #
  # @param type [PAnyType] the type to create a new function for
  # @return [Function] the created function
  # @raises ArgumentError
  #
  def self.new_function(type)
    raise ArgumentError, "Creation of new instance of type '#{type}' is not supported"
  end

  # Answers the question if instances of this type can represent themselves as a string that
  # can then be passed to the create method
  #
  # @return [Boolean] whether or not the instance has a canonical string representation
  def roundtrip_with_string?
    false
  end

  # The default instance of this type. Each type in the type system has this constant
  # declared.
  #
  DEFAULT = PAnyType.new

  protected

  # @api private
  def _assignable?(o, guard)
    o.is_a?(PAnyType)
  end

  # Produces the tuple entry at the given index given a tuple type, its from/to constraints on the last
  # type, and an index.
  # Produces nil if the index is out of bounds
  # from must be less than to, and from may not be less than 0
  #
  # @api private
  #
  def tuple_entry_at(tuple_t, to, index)
    regular = (tuple_t.types.size - 1)
    if index < regular
      tuple_t.types[index]
    elsif index < regular + to
      # in the varargs part
      tuple_t.types[-1]
    else
      nil
    end
  end

  # Applies a transformation by sending the given _method_ and _method_args_ to each of the types of the given array
  # and collecting the results in a new array. If all transformation calls returned the type instance itself (i.e. no
  # transformation took place), then this method will return `self`. If a transformation did occur, then this method
  # will either return the transformed array or in case a block was given, the result of calling a given block with
  # the transformed array.
  #
  # @param types [Array<PAnyType>] the array of types to transform
  # @param method [Symbol] The method to call on each type
  # @param method_args [Object] The arguments to pass to the method, if any
  # @return [Object] self, the transformed array, or the result of calling a given block with the transformed array
  # @yieldparam altered_types [Array<PAnyType>] the altered type array
  # @api private
  def alter_type_array(types, method, *method_args)
    modified = false
    modified_types = types.map do |t|
      t_mod = t.send(method, *method_args)
      modified ||= !t.equal?(t_mod)
      t_mod
    end
    if modified
      block_given? ? yield(modified_types) : modified_types
    else
      self
    end
  end
end

# @abstract Encapsulates common behavior for a type that contains one type
# @api public
class PTypeWithContainedType < PAnyType
  def self.register_ptype(loader, ir)
    # Abstract type. It doesn't register anything
  end

  attr_reader :type

  def initialize(type)
    @type = type
  end

  def accept(visitor, guard)
    super
    @type.accept(visitor, guard) unless @type.nil?
  end

  def generalize
    if @type.nil?
      self.class::DEFAULT
    else
      ge_type = @type.generalize
      @type.equal?(ge_type) ? self : self.class.new(ge_type)
    end
  end

  def normalize(guard = nil)
    if @type.nil?
      self.class::DEFAULT
    else
      ne_type = @type.normalize(guard)
      @type.equal?(ne_type) ? self : self.class.new(ne_type)
    end
  end

  def hash
    self.class.hash ^ @type.hash
  end

  def eql?(o)
    self.class == o.class && @type == o.type
  end

  def resolve(loader)
    rtype = @type
    rtype = rtype.resolve(loader) unless rtype.nil?
    rtype.equal?(@type) ? self : self.class.new(rtype)
  end
end

# The type of types.
# @api public
#
class PTypeType < PTypeWithContainedType
  def self.register_ptype(loader, ir)
    create_ptype(loader, ir, 'AnyType',
                 'type' => {
                   KEY_TYPE => POptionalType.new(PTypeType::DEFAULT),
                   KEY_VALUE => nil
                 })
  end

  # Returns a new function that produces a Type instance
  #
  def self.new_function(type)
    @new_function ||= Puppet::Functions.create_loaded_function(:new_type, type.loader) do
      dispatch :from_string do
        param 'String[1]', :type_string
      end

      def from_string(type_string)
        TypeParser.singleton.parse(type_string, loader)
      end
    end
  end

  def instance?(o, guard = nil)
    case o
    when PAnyType
      type.nil? || type.assignable?(o, guard)
    when Module, Puppet::Resource, Puppet::Parser::Resource
      @type.nil? ? true : assignable?(TypeCalculator.infer(o))
    else
      false
    end
  end

  def iterable?(guard = nil)
    case @type
    when PEnumType
      true
    when PIntegerType
      @type.finite_range?
    else
      false
    end
  end

  def iterable_type(guard = nil)
    # The types PIntegerType and PEnumType are Iterable
    case @type
    when PEnumType
      # @type describes the element type perfectly since the iteration is made over the
      # contained choices.
      PIterableType.new(@type)
    when PIntegerType
      # @type describes the element type perfectly since the iteration is made over the
      # specified range.
      @type.finite_range? ? PIterableType.new(@type) : nil
    else
      nil
    end
  end

  def eql?(o)
    self.class == o.class && @type == o.type
  end

  DEFAULT = PTypeType.new(nil)

  protected

  # @api private
  def _assignable?(o, guard)
    return false unless o.is_a?(PTypeType)
    return true if @type.nil? # wide enough to handle all types
    return false if o.type.nil? # wider than t

    @type.assignable?(o.type, guard)
  end
end

# For backward compatibility
PType = PTypeType

class PNotUndefType < PTypeWithContainedType
  def self.register_ptype(loader, ir)
    create_ptype(loader, ir, 'AnyType',
                 'type' => {
                   KEY_TYPE => POptionalType.new(PTypeType::DEFAULT),
                   KEY_VALUE => nil
                 })
  end

  def initialize(type = nil)
    super(type.instance_of?(PAnyType) ? nil : type)
  end

  def instance?(o, guard = nil)
    !(o.nil? || o == :undef) && (@type.nil? || @type.instance?(o, guard))
  end

  def normalize(guard = nil)
    n = super
    if n.type.nil?
      n
    elsif n.type.is_a?(POptionalType)
      PNotUndefType.new(n.type.type).normalize
    # No point in having an optional in a NotUndef
    elsif !n.type.assignable?(PUndefType::DEFAULT)
      # THe type is NotUndef anyway, so it can be stripped of
      n.type
    else
      n
    end
  end

  def new_function
    # If only NotUndef, then use Unit's null converter
    if type.nil?
      PUnitType.new_function(self)
    else
      type.new_function
    end
  end

  DEFAULT = PNotUndefType.new

  protected

  # @api private
  def _assignable?(o, guard)
    o.is_a?(PAnyType) && !o.assignable?(PUndefType::DEFAULT, guard) && (@type.nil? || @type.assignable?(o, guard))
  end
end

# @api public
#
class PUndefType < PAnyType
  def self.register_ptype(loader, ir)
    create_ptype(loader, ir, 'AnyType')
  end

  def instance?(o, guard = nil)
    o.nil? || :undef == o
  end

  # @api private
  def callable_args?(callable_t, guard)
    # if callable_t is Optional (or indeed PUndefType), this means that 'missing callable' is accepted
    callable_t.assignable?(DEFAULT, guard)
  end

  DEFAULT = PUndefType.new

  protected

  # @api private
  def _assignable?(o, guard)
    o.is_a?(PUndefType)
  end
end

# A type private to the type system that describes "ignored type" - i.e. "I am what you are"
# @api private
#
class PUnitType < PAnyType
  def self.register_ptype(loader, ir)
    create_ptype(loader, ir, 'AnyType')
  end

  def instance?(o, guard = nil)
    true
  end

  # A "null" implementation - that simply returns the given argument
  def self.new_function(type)
    @new_function ||= Puppet::Functions.create_loaded_function(:new_unit, type.loader) do
      dispatch :from_args do
        param 'Any', :from
      end

      def from_args(from)
        from
      end
    end
  end

  DEFAULT = PUnitType.new

  def assignable?(o, guard = nil)
    true
  end

  protected

  # @api private
  def _assignable?(o, guard)
    true
  end
end

# @api public
#
class PDefaultType < PAnyType
  def self.register_ptype(loader, ir)
    create_ptype(loader, ir, 'AnyType')
  end

  def instance?(o, guard = nil)
    # Ensure that Symbol.== is called here instead of something unknown
    # that is implemented on o
    :default == o
  end

  DEFAULT = PDefaultType.new

  protected

  # @api private
  def _assignable?(o, guard)
    o.is_a?(PDefaultType)
  end
end

# Type that is a Scalar
# @api public
#
class PScalarType < PAnyType
  def self.register_ptype(loader, ir)
    create_ptype(loader, ir, 'AnyType')
  end

  def instance?(o, guard = nil)
    if o.is_a?(String) || o.is_a?(Numeric) || o.is_a?(TrueClass) || o.is_a?(FalseClass) || o.is_a?(Regexp)
      true
    elsif o.instance_of?(Array) || o.instance_of?(Hash) || o.is_a?(PAnyType) || o.is_a?(NilClass)
      false
    else
      assignable?(TypeCalculator.infer(o))
    end
  end

  def roundtrip_with_string?
    true
  end

  DEFAULT = PScalarType.new

  protected

  # @api private
  def _assignable?(o, guard)
    o.is_a?(PScalarType) ||
      PStringType::DEFAULT.assignable?(o, guard) ||
      PIntegerType::DEFAULT.assignable?(o, guard) ||
      PFloatType::DEFAULT.assignable?(o, guard) ||
      PBooleanType::DEFAULT.assignable?(o, guard) ||
      PRegexpType::DEFAULT.assignable?(o, guard) ||
      PSemVerType::DEFAULT.assignable?(o, guard) ||
      PSemVerRangeType::DEFAULT.assignable?(o, guard) ||
      PTimespanType::DEFAULT.assignable?(o, guard) ||
      PTimestampType::DEFAULT.assignable?(o, guard)
  end
end

# Like Scalar but limited to Json Data.
# @api public
#
class PScalarDataType < PScalarType
  def self.register_ptype(loader, ir)
    create_ptype(loader, ir, 'ScalarType')
  end

  def instance?(o, guard = nil)
    o.instance_of?(String) || o.is_a?(Integer) || o.is_a?(Float) || o.is_a?(TrueClass) || o.is_a?(FalseClass)
  end

  DEFAULT = PScalarDataType.new

  protected

  # @api private
  def _assignable?(o, guard)
    o.is_a?(PScalarDataType) ||
      PStringType::DEFAULT.assignable?(o, guard) ||
      PIntegerType::DEFAULT.assignable?(o, guard) ||
      PFloatType::DEFAULT.assignable?(o, guard) ||
      PBooleanType::DEFAULT.assignable?(o, guard)
  end
end

# A string type describing the set of strings having one of the given values
# @api public
#
class PEnumType < PScalarDataType
  def self.register_ptype(loader, ir)
    create_ptype(loader, ir, 'ScalarDataType',
                 'values' => PArrayType.new(PStringType::NON_EMPTY),
                 'case_insensitive' => { 'type' => PBooleanType::DEFAULT, 'value' => false })
  end

  attr_reader :values, :case_insensitive

  def initialize(values, case_insensitive = false)
    @values = values.uniq.sort.freeze
    @case_insensitive = case_insensitive
  end

  def case_insensitive?
    @case_insensitive
  end

  # Returns Enumerator if no block is given, otherwise, calls the given
  # block with each of the strings for this enum
  def each(&block)
    r = Iterable.on(self)
    block_given? ? r.each(&block) : r
  end

  def generalize
    # General form of an Enum is a String
    if @values.empty?
      PStringType::DEFAULT
    else
      range = @values.map(&:size).minmax
      PStringType.new(PIntegerType.new(range.min, range.max))
    end
  end

  def iterable?(guard = nil)
    true
  end

  def iterable_type(guard = nil)
    # An instance of an Enum is a String
    PStringType::ITERABLE_TYPE
  end

  def hash
    @values.hash ^ @case_insensitive.hash
  end

  def eql?(o)
    self.class == o.class && @values == o.values && @case_insensitive == o.case_insensitive?
  end

  def instance?(o, guard = nil)
    if o.is_a?(String)
      @case_insensitive ? @values.any? { |p| p.casecmp(o) == 0 } : @values.any? { |p| p == o }
    else
      false
    end
  end

  DEFAULT = PEnumType.new(EMPTY_ARRAY)

  protected

  # @api private
  def _assignable?(o, guard)
    return true if self == o

    svalues = values
    if svalues.empty?
      return true if o.is_a?(PStringType) || o.is_a?(PEnumType) || o.is_a?(PPatternType)
    end
    case o
    when PStringType
      # if the contained string is found in the set of enums
      instance?(o.value, guard)
    when PEnumType
      !o.values.empty? && (case_insensitive? || !o.case_insensitive?) && o.values.all? { |s| instance?(s, guard) }
    else
      false
    end
  end
end

INTEGER_HEX = '(?:0[xX][0-9A-Fa-f]+)'
INTEGER_OCT = '(?:0[0-7]+)'
INTEGER_BIN = '(?:0[bB][01]+)'
INTEGER_DEC = '(?:0|[1-9]\d*)'
INTEGER_DEC_OR_OCT = '(?:\d+)'
SIGN_PREFIX = '[+-]?\s*'

OPTIONAL_FRACTION = '(?:\.\d+)?'
OPTIONAL_EXPONENT = '(?:[eE]-?\d+)?'
FLOAT_DEC = '(?:' + INTEGER_DEC + OPTIONAL_FRACTION + OPTIONAL_EXPONENT + ')'

INTEGER_PATTERN = '\A' + SIGN_PREFIX + '(?:' + INTEGER_DEC + '|' + INTEGER_HEX + '|' + INTEGER_OCT + '|' + INTEGER_BIN + ')\z'
INTEGER_PATTERN_LENIENT = '\A' + SIGN_PREFIX + '(?:' + INTEGER_DEC_OR_OCT + '|' + INTEGER_HEX + '|' + INTEGER_BIN + ')\z'
FLOAT_PATTERN = '\A' + SIGN_PREFIX + '(?:' + FLOAT_DEC + '|' + INTEGER_HEX + '|' + INTEGER_OCT + '|' + INTEGER_BIN + ')\z'

# @api public
#
class PNumericType < PScalarDataType
  def self.register_ptype(loader, ir)
    create_ptype(loader, ir, 'ScalarDataType',
                 'from' => { KEY_TYPE => POptionalType.new(PNumericType::DEFAULT), KEY_VALUE => nil },
                 'to' => { KEY_TYPE => POptionalType.new(PNumericType::DEFAULT), KEY_VALUE => nil })
  end

  def self.new_function(type)
    @new_function ||= Puppet::Functions.create_loaded_function(:new_numeric, type.loader) do
      local_types do
        type "Convertible = Variant[Integer, Float, Boolean, Pattern[/#{FLOAT_PATTERN}/], Timespan, Timestamp]"
        type 'NamedArgs   = Struct[{from => Convertible, Optional[abs] => Boolean}]'
      end

      dispatch :from_args do
        param          'Convertible',  :from
        optional_param 'Boolean',      :abs
      end

      dispatch :from_hash do
        param          'NamedArgs', :hash_args
      end

      argument_mismatch :on_error do
        param          'Any',     :from
        optional_param 'Boolean', :abs
      end

      def from_args(from, abs = false)
        result = from_convertible(from)
        abs ? result.abs : result
      end

      def from_hash(args_hash)
        from_args(args_hash['from'], args_hash['abs'] || false)
      end

      def from_convertible(from)
        case from
        when Float
          from
        when Integer
          from
        when Time::TimeData
          from.to_f
        when TrueClass
          1
        when FalseClass
          0
        else
          begin
            if from[0] == '0'
              second_char = (from[1] || '').downcase
              if second_char == 'b' || second_char == 'x'
                # use built in conversion
                return Integer(from)
              end
            end

            Puppet::Pops::Utils.to_n(from)
          rescue TypeError => e
            raise TypeConversionError, e.message
          rescue ArgumentError => e
            raise TypeConversionError, e.message
          end
        end
      end

      def on_error(from, abs = false)
        if from.is_a?(String)
          _("The string '%{str}' cannot be converted to Numeric") % { str: from }
        else
          t = TypeCalculator.singleton.infer(from).generalize
          _("Value of type %{type} cannot be converted to Numeric") % { type: t }
        end
      end
    end
  end

  def initialize(from, to = Float::INFINITY)
    from = -Float::INFINITY if from.nil? || from == :default
    to = Float::INFINITY if to.nil? || to == :default
    raise ArgumentError, "'from' must be less or equal to 'to'. Got (#{from}, #{to}" if from > to

    @from = from
    @to = to
  end

  # Checks if this numeric range intersects with another
  #
  # @param o [PNumericType] the range to compare with
  # @return [Boolean] `true` if this range intersects with the other range
  # @api public
  def intersect?(o)
    instance_of?(o.class) && !(@to < o.numeric_from || o.numeric_to < @from)
  end

  # Returns the lower bound of the numeric range or `nil` if no lower bound is set.
  # @return [Float,Integer]
  def from
    @from == -Float::INFINITY ? nil : @from
  end

  # Returns the upper bound of the numeric range or `nil` if no upper bound is set.
  # @return [Float,Integer]
  def to
    @to == Float::INFINITY ? nil : @to
  end

  # Same as #from but will return `-Float::Infinity` instead of `nil` if no lower bound is set.
  # @return [Float,Integer]
  def numeric_from
    @from
  end

  # Same as #to but will return `Float::Infinity` instead of `nil` if no lower bound is set.
  # @return [Float,Integer]
  def numeric_to
    @to
  end

  def hash
    @from.hash ^ @to.hash
  end

  def eql?(o)
    self.class == o.class && @from == o.numeric_from && @to == o.numeric_to
  end

  def instance?(o, guard = nil)
    (o.is_a?(Float) || o.is_a?(Integer)) && o >= @from && o <= @to
  end

  def unbounded?
    @from == -Float::INFINITY && @to == Float::INFINITY
  end

  protected

  # @api_private
  def _assignable?(o, guard)
    return false unless o.is_a?(self.class)

    # If o min and max are within the range of t
    @from <= o.numeric_from && @to >= o.numeric_to
  end

  DEFAULT = PNumericType.new(-Float::INFINITY)
end

# @api public
#
class PIntegerType < PNumericType
  def self.register_ptype(loader, ir)
    create_ptype(loader, ir, 'NumericType')
  end

  # Will respond `true` for any range that is bounded at both ends.
  #
  # @return [Boolean] `true` if the type describes a finite range.
  def finite_range?
    @from != -Float::INFINITY && @to != Float::INFINITY
  end

  def generalize
    DEFAULT
  end

  def instance?(o, guard = nil)
    o.is_a?(Integer) && o >= numeric_from && o <= numeric_to
  end

  # Checks if this range is adjacent to the given range
  #
  # @param o [PIntegerType] the range to compare with
  # @return [Boolean] `true` if this range is adjacent to the other range
  # @api public
  def adjacent?(o)
    o.is_a?(PIntegerType) && (@to + 1 == o.from || o.to + 1 == @from)
  end

  # Concatenates this range with another range provided that the ranges intersect or
  # are adjacent. When that's not the case, this method will return `nil`
  #
  # @param o [PIntegerType] the range to concatenate with this range
  # @return [PIntegerType,nil] the concatenated range or `nil` when the ranges were apart
  # @api public
  def merge(o)
    if intersect?(o) || adjacent?(o)
      min = @from <= o.numeric_from ? @from : o.numeric_from
      max = @to >= o.numeric_to ? @to : o.numeric_to
      PIntegerType.new(min, max)
    else
      nil
    end
  end

  def iterable?(guard = nil)
    true
  end

  def iterable_type(guard = nil)
    # It's unknown if the iterable will be a range (min, max) or a "times" (0, max)
    PIterableType.new(PIntegerType::DEFAULT)
  end

  # Returns Float.Infinity if one end of the range is unbound
  def size
    return Float::INFINITY if @from == -Float::INFINITY || @to == Float::INFINITY

    1 + (to - from).abs
  end

  # Returns the range as an array ordered so the smaller number is always first.
  # The number may be Infinity or -Infinity.
  def range
    [@from, @to]
  end

  # Returns Enumerator if no block is given
  # Returns nil if size is infinity (does not yield)
  def each(&block)
    r = Iterable.on(self)
    block_given? ? r.each(&block) : r
  end

  # Returns a range where both to and from are positive numbers. Negative
  # numbers are converted to zero
  # @return [PIntegerType] a positive range
  def to_size
    if @from >= 0
      self
    else
      PIntegerType.new(0, @to < 0 ? 0 : @to)
    end
  end

  def new_function
    @@new_function ||= Puppet::Functions.create_loaded_function(:new, loader) do
      local_types do
        type 'Radix       = Variant[Default, Integer[2,2], Integer[8,8], Integer[10,10], Integer[16,16]]'
        type "Convertible = Variant[Numeric, Boolean, Pattern[/#{INTEGER_PATTERN_LENIENT}/], Timespan, Timestamp]"
        type 'NamedArgs   = Struct[{from => Convertible, Optional[radix] => Radix, Optional[abs] => Boolean}]'
      end

      dispatch :from_args do
        param          'Convertible', :from
        optional_param 'Radix',   :radix
        optional_param 'Boolean', :abs
      end

      dispatch :from_hash do
        param          'NamedArgs', :hash_args
      end

      argument_mismatch :on_error_hash do
        param          'Hash', :hash_args
      end

      argument_mismatch :on_error do
        param          'Any',     :from
        optional_param 'Integer', :radix
        optional_param 'Boolean', :abs
      end

      def from_args(from, radix = :default, abs = false)
        result = from_convertible(from, radix)
        abs ? result.abs : result
      end

      def from_hash(args_hash)
        from_args(args_hash['from'], args_hash['radix'] || :default, args_hash['abs'] || false)
      end

      def from_convertible(from, radix)
        case from
        when Float, Time::TimeData
          from.to_i
        when Integer
          from
        when TrueClass
          1
        when FalseClass
          0
        else
          begin
            radix == :default ? Integer(from) : Integer(from, radix)
          rescue TypeError => e
            raise TypeConversionError, e.message
          rescue ArgumentError => e
            # Test for special case where there is whitespace between sign and number
            match = Patterns::WS_BETWEEN_SIGN_AND_NUMBER.match(from)
            if match
              begin
                # Try again, this time with whitespace removed
                return from_args(match[1] + match[2], radix)
              rescue TypeConversionError
                # Ignored to retain original error
              end
            end
            raise TypeConversionError, e.message
          end
        end
      end

      def on_error_hash(args_hash)
        if args_hash.include?('from')
          from = args_hash['from']
          return on_error(from) unless loader.load(:type, 'convertible').instance?(from)
        end
        radix = args_hash['radix']
        assert_radix(radix) unless radix.nil? || radix == :default
        TypeAsserter.assert_instance_of('Integer.new', loader.load(:type, 'namedargs'), args_hash)
      end

      def on_error(from, radix = :default, abs = nil)
        assert_radix(radix) unless radix == :default
        if from.is_a?(String)
          _("The string '%{str}' cannot be converted to Integer") % { str: from }
        else
          t = TypeCalculator.singleton.infer(from).generalize
          _("Value of type %{type} cannot be converted to Integer") % { type: t }
        end
      end

      def assert_radix(radix)
        case radix
        when 2, 8, 10, 16
          # do nothing
        else
          raise ArgumentError, _("Illegal radix: %{radix}, expected 2, 8, 10, 16, or default") % { radix: radix }
        end
        radix
      end
    end
  end

  DEFAULT = PIntegerType.new(-Float::INFINITY)
end

# @api public
#
class PFloatType < PNumericType
  def self.register_ptype(loader, ir)
    create_ptype(loader, ir, 'NumericType')
  end

  def generalize
    DEFAULT
  end

  def instance?(o, guard = nil)
    o.is_a?(Float) && o >= numeric_from && o <= numeric_to
  end

  # Concatenates this range with another range provided that the ranges intersect. When that's not the case, this
  # method will return `nil`
  #
  # @param o [PFloatType] the range to concatenate with this range
  # @return [PFloatType,nil] the concatenated range or `nil` when the ranges were apart
  # @api public
  def merge(o)
    if intersect?(o)
      min = @from <= o.from ? @from : o.from
      max = @to >= o.to ? @to : o.to
      PFloatType.new(min, max)
    else
      nil
    end
  end

  # Returns a new function that produces a Float value
  #
  def self.new_function(type)
    @new_function ||= Puppet::Functions.create_loaded_function(:new_float, type.loader) do
      local_types do
        type "Convertible = Variant[Numeric, Boolean, Pattern[/#{FLOAT_PATTERN}/], Timespan, Timestamp]"
        type 'NamedArgs   = Struct[{from => Convertible, Optional[abs] => Boolean}]'
      end

      dispatch :from_args do
        param          'Convertible',  :from
        optional_param 'Boolean',      :abs
      end

      dispatch :from_hash do
        param          'NamedArgs', :hash_args
      end

      argument_mismatch :on_error do
        param          'Any',     :from
        optional_param 'Boolean', :abs
      end

      def from_args(from, abs = false)
        result = from_convertible(from)
        abs ? result.abs : result
      end

      def from_hash(args_hash)
        from_args(args_hash['from'], args_hash['abs'] || false)
      end

      def from_convertible(from)
        case from
        when Float
          from
        when Integer
          Float(from)
        when Time::TimeData
          from.to_f
        when TrueClass
          1.0
        when FalseClass
          0.0
        else
          begin
            # support a binary as float
            if from[0] == '0' && from[1].casecmp('b').zero?
              from = Integer(from)
            end
            Float(from)
          rescue TypeError => e
            raise TypeConversionError, e.message
          rescue ArgumentError => e
            # Test for special case where there is whitespace between sign and number
            match = Patterns::WS_BETWEEN_SIGN_AND_NUMBER.match(from)
            if match
              begin
                # Try again, this time with whitespace removed
                return from_args(match[1] + match[2])
              rescue TypeConversionError
                # Ignored to retain original error
              end
            end
            raise TypeConversionError, e.message
          end
        end
      end

      def on_error(from, _ = false)
        if from.is_a?(String)
          _("The string '%{str}' cannot be converted to Float") % { str: from }
        else
          t = TypeCalculator.singleton.infer(from).generalize
          _("Value of type %{type} cannot be converted to Float") % { type: t }
        end
      end
    end
  end

  DEFAULT = PFloatType.new(-Float::INFINITY)
end

# @api public
#
class PCollectionType < PAnyType
  def self.register_ptype(loader, ir)
    create_ptype(loader, ir, 'AnyType',
                 'size_type' => {
                   KEY_TYPE => POptionalType.new(PTypeType.new(PIntegerType::DEFAULT)),
                   KEY_VALUE => nil
                 })
  end

  attr_reader :size_type

  def initialize(size_type)
    @size_type = size_type.nil? ? nil : size_type.to_size
  end

  def accept(visitor, guard)
    super
    @size_type.accept(visitor, guard) unless @size_type.nil?
  end

  def generalize
    DEFAULT
  end

  def normalize(guard = nil)
    DEFAULT
  end

  def instance?(o, guard = nil)
    # The inferred type of a class derived from Array or Hash is either Runtime or Object. It's not assignable to the Collection type.
    if o.instance_of?(Array) || o.instance_of?(Hash)
      @size_type.nil? || @size_type.instance?(o.size)
    else
      false
    end
  end

  # Returns an array with from (min) size to (max) size
  def size_range
    (@size_type || DEFAULT_SIZE).range
  end

  def has_empty_range?
    from, to = size_range
    from == 0 && to == 0
  end

  def hash
    @size_type.hash
  end

  def iterable?(guard = nil)
    true
  end

  def eql?(o)
    self.class == o.class && @size_type == o.size_type
  end

  DEFAULT_SIZE = PIntegerType.new(0)
  ZERO_SIZE = PIntegerType.new(0, 0)
  NOT_EMPTY_SIZE = PIntegerType.new(1)
  DEFAULT = PCollectionType.new(nil)

  protected

  # @api private
  #
  def _assignable?(o, guard)
    case o
    when PCollectionType
      (@size_type || DEFAULT_SIZE).assignable?(o.size_type || DEFAULT_SIZE, guard)
    when PTupleType
      # compute the tuple's min/max size, and check if that size matches
      size_s = size_type || DEFAULT_SIZE
      size_o = o.size_type
      if size_o.nil?
        type_count = o.types.size
        size_o = PIntegerType.new(type_count, type_count)
      end
      size_s.assignable?(size_o)
    when PStructType
      from = to = o.elements.size
      (@size_type || DEFAULT_SIZE).assignable?(PIntegerType.new(from, to), guard)
    else
      false
    end
  end
end

class PIterableType < PTypeWithContainedType
  def self.register_ptype(loader, ir)
    create_ptype(loader, ir, 'AnyType',
                 'type' => {
                   KEY_TYPE => POptionalType.new(PTypeType::DEFAULT),
                   KEY_VALUE => nil
                 })
  end

  def element_type
    @type
  end

  def instance?(o, guard = nil)
    if @type.nil? || @type.assignable?(PAnyType::DEFAULT, guard)
      # Any element_type will do
      case o
      when Iterable, String, Hash, Array, Range, PEnumType
        true
      when Integer
        o >= 0
      when PIntegerType
        o.finite_range?
      when PTypeAliasType
        instance?(o.resolved_type, guard)
      else
        false
      end
    else
      assignable?(TypeCalculator.infer(o), guard)
    end
  end

  def iterable?(guard = nil)
    true
  end

  def iterable_type(guard = nil)
    self
  end

  DEFAULT = PIterableType.new(nil)

  protected

  # @api private
  def _assignable?(o, guard)
    if @type.nil? || @type.assignable?(PAnyType::DEFAULT, guard)
      # Don't request the iterable_type. Since this Iterable accepts Any element, it is enough that o is iterable.
      o.iterable?
    else
      o = o.iterable_type
      o.nil? || o.element_type.nil? ? false : @type.assignable?(o.element_type, guard)
    end
  end
end

# @api public
#
class PIteratorType < PTypeWithContainedType
  def self.register_ptype(loader, ir)
    create_ptype(loader, ir, 'AnyType',
                 'type' => {
                   KEY_TYPE => POptionalType.new(PTypeType::DEFAULT),
                   KEY_VALUE => nil
                 })
  end

  def element_type
    @type
  end

  def instance?(o, guard = nil)
    o.is_a?(Iterable) && (@type.nil? || @type.assignable?(o.element_type, guard))
  end

  def iterable?(guard = nil)
    true
  end

  def iterable_type(guard = nil)
    @type.nil? ? PIterableType::DEFAULT : PIterableType.new(@type)
  end

  DEFAULT = PIteratorType.new(nil)

  protected

  # @api private
  def _assignable?(o, guard)
    o.is_a?(PIteratorType) && (@type.nil? || @type.assignable?(o.element_type, guard))
  end
end

# @api public
#
class PStringType < PScalarDataType
  def self.register_ptype(loader, ir)
    create_ptype(loader, ir, 'ScalarDataType',
                 'size_type_or_value' => {
                   KEY_TYPE => POptionalType.new(PVariantType.new([PStringType::DEFAULT, PTypeType.new(PIntegerType::DEFAULT)])),
                   KEY_VALUE => nil
                 })
  end

  attr_reader :size_type_or_value

  def initialize(size_type_or_value, deprecated_multi_args = EMPTY_ARRAY)
    unless deprecated_multi_args.empty?
      if Puppet[:strict] != :off
        # TRANSLATORS 'PStringType#initialize' is a class and method name and should not be translated
        Puppet.warn_once('deprecations', "PStringType#initialize_multi_args",
                         _("Passing more than one argument to PStringType#initialize is deprecated"))
      end
      size_type_or_value = deprecated_multi_args[0]
    end
    @size_type_or_value = size_type_or_value.is_a?(PIntegerType) ? size_type_or_value.to_size : size_type_or_value
  end

  def accept(visitor, guard)
    super
    @size_type_or_value.accept(visitor, guard) if @size_type_or_value.is_a?(PIntegerType)
  end

  def generalize
    DEFAULT
  end

  def hash
    @size_type_or_value.hash
  end

  def iterable?(guard = nil)
    true
  end

  def iterable_type(guard = nil)
    ITERABLE_TYPE
  end

  def eql?(o)
    self.class == o.class && @size_type_or_value == o.size_type_or_value
  end

  def instance?(o, guard = nil)
    # true if size compliant
    if o.is_a?(String)
      if @size_type_or_value.is_a?(PIntegerType)
        @size_type_or_value.instance?(o.size, guard)
      else
        @size_type_or_value.nil? ? true : o == value
      end
    else
      false
    end
  end

  def value
    @size_type_or_value.is_a?(PIntegerType) ? nil : @size_type_or_value
  end

  # @deprecated
  # @api private
  def values
    if Puppet[:strict] != :off
      # TRANSLATORS 'PStringType#values' and '#value' are classes and method names and should not be translated
      Puppet.warn_once('deprecations', "PStringType#values", _("Method PStringType#values is deprecated. Use #value instead"))
    end
    @value.is_a?(String) ? [@value] : EMPTY_ARRAY
  end

  def size_type
    @size_type_or_value.is_a?(PIntegerType) ? @size_type_or_value : nil
  end

  def derived_size_type
    case @size_type_or_value
    when PIntegerType
      @size_type_or_value
    when String
      sz = @size_type_or_value.size
      PIntegerType.new(sz, sz)
    else
      PCollectionType::DEFAULT_SIZE
    end
  end

  def self.new_function(type)
    @new_function ||= Puppet::Functions.create_loaded_function(:new_string, type.loader) do
      local_types do
        type "Format = Pattern[/#{StringConverter::Format::FMT_PATTERN_STR}/]"
        type 'ContainerFormat = Struct[{
          Optional[format]         => Format,
          Optional[separator]      => String,
          Optional[separator2]     => String,
          Optional[string_formats] => Hash[Type, Format]
        }]'
        type 'TypeMap = Hash[Type, Variant[Format, ContainerFormat]]'
        type 'Convertible = Any'
        type 'Formats = Variant[Default, String[1], TypeMap]'
      end

      dispatch :from_args do
        param           'Convertible',  :from
        optional_param  'Formats',      :string_formats
      end

      def from_args(from, formats = :default)
        StringConverter.singleton.convert(from, formats)
      end
    end
  end

  DEFAULT = PStringType.new(nil)
  NON_EMPTY = PStringType.new(PCollectionType::NOT_EMPTY_SIZE)

  # Iterates over each character of the string
  ITERABLE_TYPE = PIterableType.new(PStringType.new(PIntegerType.new(1, 1)))

  protected

  # @api private
  def _assignable?(o, guard)
    if @size_type_or_value.is_a?(PIntegerType)
      # A general string is assignable by any other string or pattern restricted string
      # if the string has a size constraint it does not match since there is no reasonable way
      # to compute the min/max length a pattern will match. For enum, it is possible to test that
      # each enumerator value is within range
      case o
      when PStringType
        @size_type_or_value.assignable?(o.derived_size_type, guard)

      when PEnumType
        if o.values.empty?
          # enum represents all enums, and thus all strings, a sized constrained string can thus not
          # be assigned any enum (unless it is max size).
          @size_type_or_value.assignable?(PCollectionType::DEFAULT_SIZE, guard)
        else
          # true if all enum values are within range
          orange = o.values.map(&:size).minmax
          srange = @size_type_or_value.range
          # If o min and max are within the range of t
          srange[0] <= orange[0] && srange[1] >= orange[1]
        end

      when PPatternType
        # true if size constraint is at least 0 to +Infinity (which is the same as the default)
        @size_type_or_value.assignable?(PCollectionType::DEFAULT_SIZE, guard)
      else
        # no other type matches string
        false
      end
    else
      case o
      when PStringType
        # Must match exactly when value is a string
        @size_type_or_value.nil? || @size_type_or_value == o.size_type_or_value
      when PEnumType
        @size_type_or_value.nil? ? true : o.values.size == 1 && !o.case_insensitive? && o.values[0]
      when PPatternType
        @size_type_or_value.nil?
      else
        # All others are false, since no other type describes the same set of specific strings
        false
      end
    end
  end
end

# @api public
#
class PRegexpType < PScalarType
  def self.register_ptype(loader, ir)
    create_ptype(loader, ir, 'ScalarType',
                 'pattern' => {
                   KEY_TYPE => PVariantType.new([PUndefType::DEFAULT, PStringType::DEFAULT, PRegexpType::DEFAULT]),
                   KEY_VALUE => nil
                 })
  end

  # Returns a new function that produces a Regexp instance
  #
  def self.new_function(type)
    @new_function ||= Puppet::Functions.create_loaded_function(:new_float, type.loader) do
      dispatch :from_string do
        param 'String', :pattern
        optional_param 'Boolean', :escape
      end

      def from_string(pattern, escape = false)
        Regexp.new(escape ? Regexp.escape(pattern) : pattern)
      end
    end
  end

  attr_reader :pattern

  # @param regexp [Regexp] the regular expression
  # @return [String] the Regexp as a slash delimited string with slashes escaped
  def self.regexp_to_s_with_delimiters(regexp)
    regexp.options == 0 ? regexp.inspect : "/#{regexp}/"
  end

  # @param regexp [Regexp] the regular expression
  # @return [String] the Regexp as a string without escaped slash
  def self.regexp_to_s(regexp)
    append_flags_group(regexp.source, regexp.options)
  end

  def self.append_flags_group(rx_string, options)
    if options == 0
      rx_string
    else
      bld = '(?'.dup
      bld << 'i' if (options & Regexp::IGNORECASE) != 0
      bld << 'm' if (options & Regexp::MULTILINE) != 0
      bld << 'x' if (options & Regexp::EXTENDED) != 0
      unless options == (Regexp::IGNORECASE | Regexp::MULTILINE | Regexp::EXTENDED)
        bld << '-'
        bld << 'i' if (options & Regexp::IGNORECASE) == 0
        bld << 'm' if (options & Regexp::MULTILINE) == 0
        bld << 'x' if (options & Regexp::EXTENDED) == 0
      end
      bld << ':' << rx_string << ')'
      bld.freeze
    end
  end

  def initialize(pattern)
    if pattern.is_a?(Regexp)
      @regexp = pattern
      @pattern = PRegexpType.regexp_to_s(pattern)
    else
      @pattern = pattern
    end
  end

  def regexp
    @regexp ||= Regexp.new(@pattern || '')
  end

  def hash
    @pattern.hash
  end

  def eql?(o)
    self.class == o.class && @pattern == o.pattern
  end

  def instance?(o, guard = nil)
    o.is_a?(Regexp) && @pattern.nil? || regexp == o
  end

  DEFAULT = PRegexpType.new(nil)

  protected

  # @api private
  #
  def _assignable?(o, guard)
    o.is_a?(PRegexpType) && (@pattern.nil? || @pattern == o.pattern)
  end
end

# Represents a subtype of String that narrows the string to those matching the patterns
# If specified without a pattern it is basically the same as the String type.
#
# @api public
#
class PPatternType < PScalarDataType
  def self.register_ptype(loader, ir)
    create_ptype(loader, ir, 'ScalarDataType', 'patterns' => PArrayType.new(PRegexpType::DEFAULT))
  end

  attr_reader :patterns

  def initialize(patterns)
    @patterns = patterns.freeze
  end

  def accept(visitor, guard)
    super
    @patterns.each { |p| p.accept(visitor, guard) }
  end

  def hash
    @patterns.hash
  end

  def eql?(o)
    self.class == o.class && @patterns.size == o.patterns.size && (@patterns - o.patterns).empty?
  end

  def instance?(o, guard = nil)
    o.is_a?(String) && (@patterns.empty? || @patterns.any? { |p| p.regexp.match(o) })
  end

  DEFAULT = PPatternType.new(EMPTY_ARRAY)

  protected

  # @api private
  #
  def _assignable?(o, guard)
    return true if self == o

    case o
    when PStringType
      v = o.value
      if v.nil?
        # Strings cannot all match a pattern, but if there is no pattern it is ok
        # (There should really always be a pattern, but better safe than sorry).
        @patterns.empty?
      else
        # the string in String type must match one of the patterns in Pattern type,
        # or Pattern represents all Patterns == all Strings
        regexps = @patterns.map(&:regexp)
        regexps.empty? || regexps.any? { |re| re.match(v) }
      end
    when PEnumType
      if o.values.empty?
        # Enums (unknown which ones) cannot all match a pattern, but if there is no pattern it is ok
        # (There should really always be a pattern, but better safe than sorry).
        @patterns.empty?
      else
        # all strings in String/Enum type must match one of the patterns in Pattern type,
        # or Pattern represents all Patterns == all Strings
        regexps = @patterns.map(&:regexp)
        regexps.empty? || o.values.all? { |s| regexps.any? { |re| re.match(s) } }
      end
    when PPatternType
      @patterns.empty?
    else
      false
    end
  end
end

# @api public
#
class PBooleanType < PScalarDataType
  def self.register_ptype(loader, ir)
    create_ptype(loader, ir, 'ScalarDataType')
  end

  attr_reader :value

  def initialize(value = nil)
    @value = value
  end

  def eql?(o)
    o.is_a?(PBooleanType) && @value == o.value
  end

  def generalize
    PBooleanType::DEFAULT
  end

  def hash
    31 ^ @value.hash
  end

  def instance?(o, guard = nil)
    (o == true || o == false) && (@value.nil? || value == o)
  end

  def self.new_function(type)
    @new_function ||= Puppet::Functions.create_loaded_function(:new_boolean, type.loader) do
      dispatch :from_args do
        param "Variant[Integer, Float, Boolean, Enum['false','true','yes','no','y','n',true]]", :from
      end

      argument_mismatch :on_error do
        param  'Any', :from
      end

      def from_args(from)
        from = from.downcase if from.is_a?(String)
        case from
        when Float, Integer
          !from.zero?
        when false, 'false', 'no', 'n'
          false
        else
          true
        end
      end

      def on_error(from)
        if from.is_a?(String)
          _("The string '%{str}' cannot be converted to Boolean") % { str: from }
        else
          t = TypeCalculator.singleton.infer(from).generalize
          _("Value of type %{type} cannot be converted to Boolean") % { type: t }
        end
      end
    end
  end

  DEFAULT = PBooleanType.new
  TRUE = PBooleanType.new(true)
  FALSE = PBooleanType.new(false)

  protected

  # @api private
  #
  def _assignable?(o, guard)
    o.is_a?(PBooleanType) && (@value.nil? || @value == o.value)
  end
end

# @api public
#
# @api public
#
class PStructElement < TypedModelObject
  def self.register_ptype(loader, ir)
    @type = Pcore.create_object_type(loader, ir, self, 'Pcore::StructElement', nil,
                                     'key_type' => PTypeType::DEFAULT,
                                     'value_type' => PTypeType::DEFAULT)
  end

  attr_accessor :key_type, :value_type

  def accept(visitor, guard)
    @key_type.accept(visitor, guard)
    @value_type.accept(visitor, guard)
  end

  def hash
    value_type.hash ^ key_type.hash
  end

  def name
    k = key_type
    k = k.optional_type if k.is_a?(POptionalType)
    k.value
  end

  def initialize(key_type, value_type)
    @key_type = key_type
    @value_type = value_type
  end

  def generalize
    gv_type = @value_type.generalize
    @value_type.equal?(gv_type) ? self : PStructElement.new(@key_type, gv_type)
  end

  def normalize(guard = nil)
    nv_type = @value_type.normalize(guard)
    @value_type.equal?(nv_type) ? self : PStructElement.new(@key_type, nv_type)
  end

  def resolve(loader)
    rkey_type = @key_type.resolve(loader)
    rvalue_type = @value_type.resolve(loader)
    rkey_type.equal?(@key_type) && rvalue_type.equal?(@value_type) ? self : self.class.new(rkey_type, rvalue_type)
  end

  def <=>(o)
    name <=> o.name
  end

  def eql?(o)
    self == o
  end

  def ==(o)
    self.class == o.class && value_type == o.value_type && key_type == o.key_type
  end

  # Special boostrap method to overcome the hen and egg problem with the Object initializer that contains
  # types that are derived from Object (such as Annotation)
  #
  # @api private
  def replace_value_type(new_type)
    @value_type = new_type
  end
end

# @api public
#
class PStructType < PAnyType
  include Enumerable

  def self.register_ptype(loader, ir)
    create_ptype(loader, ir, 'AnyType', 'elements' => PArrayType.new(PTypeReferenceType.new('Pcore::StructElement')))
  end

  def initialize(elements)
    @elements = elements.freeze
  end

  def accept(visitor, guard)
    super
    @elements.each { |elem| elem.accept(visitor, guard) }
  end

  def each
    if block_given?
      elements.each { |elem| yield elem }
    else
      elements.to_enum
    end
  end

  def generalize
    if @elements.empty?
      DEFAULT
    else
      alter_type_array(@elements, :generalize) { |altered| PStructType.new(altered) }
    end
  end

  def normalize(guard = nil)
    if @elements.empty?
      DEFAULT
    else
      alter_type_array(@elements, :normalize, guard) { |altered| PStructType.new(altered) }
    end
  end

  # rubocop:disable Naming/MemoizedInstanceVariableName
  def hashed_elements
    @hashed ||= @elements.each_with_object({}) { |e, memo| memo[e.name] = e; }
  end
  # rubocop:enable Naming/MemoizedInstanceVariableName

  def hash
    @elements.hash
  end

  def iterable?(guard = nil)
    true
  end

  def iterable_type(guard = nil)
    if self == DEFAULT
      PIterableType.new(PHashType::DEFAULT_KEY_PAIR_TUPLE)
    else
      PIterableType.new(
        PTupleType.new([
                         PVariantType.maybe_create(@elements.map(&:key_type)),
                         PVariantType.maybe_create(@elements.map(&:value_type))
                       ],
                       PHashType::KEY_PAIR_TUPLE_SIZE)
      )
    end
  end

  def resolve(loader)
    changed = false
    relements = @elements.map do |elem|
      relem = elem.resolve(loader)
      changed ||= !relem.equal?(elem)
      relem
    end
    changed ? self.class.new(relements) : self
  end

  def eql?(o)
    self.class == o.class && @elements == o.elements
  end

  def elements
    @elements
  end

  def instance?(o, guard = nil)
    # The inferred type of a class derived from Hash is either Runtime or Object. It's not assignable to the Struct type.
    return false unless o.instance_of?(Hash)

    matched = 0
    @elements.all? do |e|
      key = e.name
      v = o[key]
      if v.nil? && !o.include?(key)
        # Entry is missing. Only OK when key is optional
        e.key_type.assignable?(PUndefType::DEFAULT, guard)
      else
        matched += 1
        e.value_type.instance?(v, guard)
      end
    end && matched == o.size
  end

  def new_function
    # Simply delegate to Hash type and let the higher level assertion deal with
    # compliance with the Struct type regarding the produced result.
    PHashType.new_function(self)
  end

  DEFAULT = PStructType.new(EMPTY_ARRAY)

  protected

  # @api private
  def _assignable?(o, guard)
    case o
    when Types::PStructType
      h2 = o.hashed_elements
      matched = 0
      elements.all? do |e1|
        e2 = h2[e1.name]
        if e2.nil?
          e1.key_type.assignable?(PUndefType::DEFAULT, guard)
        else
          matched += 1
          e1.key_type.assignable?(e2.key_type, guard) && e1.value_type.assignable?(e2.value_type, guard)
        end
      end && matched == h2.size
    when Types::PHashType
      required = 0
      required_elements_assignable = elements.all? do |e|
        key_type = e.key_type
        if key_type.assignable?(PUndefType::DEFAULT)
          # Element is optional so Hash does not need to provide it
          true
        else
          required += 1
          if e.value_type.assignable?(o.value_type, guard)
            # Hash must have something that is assignable. We don't care about the name or size of the key though
            # because we have no instance of a hash to compare against.
            key_type.generalize.assignable?(o.key_type)
          else
            false
          end
        end
      end
      if required_elements_assignable
        size_o = o.size_type || PCollectionType::DEFAULT_SIZE
        PIntegerType.new(required, elements.size).assignable?(size_o, guard)
      else
        false
      end
    else
      false
    end
  end
end

# @api public
#
class PTupleType < PAnyType
  include Enumerable

  def self.register_ptype(loader, ir)
    create_ptype(loader, ir, 'AnyType',
                 'types' => PArrayType.new(PTypeType::DEFAULT),
                 'size_type' => {
                   KEY_TYPE => POptionalType.new(PTypeType.new(PIntegerType::DEFAULT)),
                   KEY_VALUE => nil
                 })
  end

  # If set, describes min and max required of the given types - if max > size of
  # types, the last type entry repeats
  #
  attr_reader :size_type

  attr_reader :types

  def accept(visitor, guard)
    super
    @size_type.accept(visitor, guard) unless @size_type.nil?
    @types.each { |elem| elem.accept(visitor, guard) }
  end

  # @api private
  def callable_args?(callable_t, guard)
    unless size_type.nil?
      raise ArgumentError, 'Callable tuple may not have a size constraint when used as args'
    end

    params_tuple = callable_t.param_types
    param_block_t = callable_t.block_type
    arg_types = @types
    arg_block_t = arg_types.last
    if arg_block_t.kind_of_callable?(true, guard)
      # Can't pass a block to a callable that doesn't accept one
      return false if param_block_t.nil?

      # Check that the block is of the right tyṕe
      return false unless param_block_t.assignable?(arg_block_t, guard)

      # Check other arguments
      arg_count = arg_types.size - 1
      params_size_t = params_tuple.size_type || PIntegerType.new(*params_tuple.size_range)
      return false unless params_size_t.assignable?(PIntegerType.new(arg_count, arg_count), guard)

      ctypes = params_tuple.types
      arg_count.times do |index|
        return false unless (ctypes[index] || ctypes[-1]).assignable?(arg_types[index], guard)
      end
      return true
    end

    # Check that tuple is assignable and that the block (if declared) is optional
    params_tuple.assignable?(self, guard) && (param_block_t.nil? || param_block_t.assignable?(PUndefType::DEFAULT, guard))
  end

  def initialize(types, size_type = nil)
    @types = types
    @size_type = size_type.nil? ? nil : size_type.to_size
  end

  # Returns Enumerator for the types if no block is given, otherwise, calls the given
  # block with each of the types in this tuple
  def each
    if block_given?
      types.each { |x| yield x }
    else
      types.to_enum
    end
  end

  def generalize
    if self == DEFAULT
      DEFAULT
    else
      alter_type_array(@types, :generalize) { |altered_types| PTupleType.new(altered_types, @size_type) }
    end
  end

  def normalize(guard = nil)
    if self == DEFAULT
      DEFAULT
    else
      alter_type_array(@types, :normalize, guard) { |altered_types| PTupleType.new(altered_types, @size_type) }
    end
  end

  def resolve(loader)
    changed = false
    rtypes = @types.map do |type|
      rtype = type.resolve(loader)
      changed ||= !rtype.equal?(type)
      rtype
    end
    changed ? self.class.new(rtypes, @size_type) : self
  end

  def instance?(o, guard = nil)
    # The inferred type of a class derived from Array is either Runtime or Object. It's not assignable to the Tuple type.
    return false unless o.instance_of?(Array)

    if @size_type
      return false unless @size_type.instance?(o.size, guard)
    else
      return false unless @types.empty? || @types.size == o.size
    end
    index = -1
    @types.empty? || o.all? do |element|
      @types.fetch(index += 1) { @types.last }.instance?(element, guard)
    end
  end

  def iterable?(guard = nil)
    true
  end

  def iterable_type(guard = nil)
    PIterableType.new(PVariantType.maybe_create(types))
  end

  # Returns the number of elements accepted [min, max] in the tuple
  def size_range
    if @size_type.nil?
      types_size = @types.size
      types_size == 0 ? [0, Float::INFINITY] : [types_size, types_size]
    else
      @size_type.range
    end
  end

  # Returns the number of accepted occurrences [min, max] of the last type in the tuple
  # The defaults is [1,1]
  #
  def repeat_last_range
    if @size_type.nil?
      return [1, 1]
    end

    types_size = @types.size
    from, to = @size_type.range
    min = from - (types_size - 1)
    min = min <= 0 ? 0 : min
    max = to - (types_size - 1)
    [min, max]
  end

  def hash
    @size_type.hash ^ @types.hash
  end

  def eql?(o)
    self.class == o.class && @types == o.types && @size_type == o.size_type
  end

  def new_function
    # Simply delegate to Array type and let the higher level assertion deal with
    # compliance with the Tuple type regarding the produced result.
    PArrayType.new_function(self)
  end

  DEFAULT = PTupleType.new(EMPTY_ARRAY)

  protected

  # @api private
  def _assignable?(o, guard)
    return true if self == o
    return false unless o.is_a?(PTupleType) || o.is_a?(PArrayType)

    s_types = types
    size_s = size_type || PIntegerType.new(*size_range)

    if o.is_a?(PTupleType)
      size_o = o.size_type || PIntegerType.new(*o.size_range)
      return false unless size_s.assignable?(size_o, guard)

      unless s_types.empty?
        o_types = o.types
        return size_s.numeric_from == 0 if o_types.empty?

        o_types.size.times do |index|
          return false unless (s_types[index] || s_types[-1]).assignable?(o_types[index], guard)
        end
      end
    else
      size_o = o.size_type || PCollectionType::DEFAULT_SIZE
      return false unless size_s.assignable?(size_o, guard)

      unless s_types.empty?
        o_entry = o.element_type
        # Array of anything can not be assigned (unless tuple is tuple of anything) - this case
        # was handled at the top of this method.
        #
        return false if o_entry.nil?

        [s_types.size, size_o.range[1]].min.times { |index| return false unless (s_types[index] || s_types[-1]).assignable?(o_entry, guard) }
      end
    end
    true
  end
end

# @api public
#
class PCallableType < PAnyType
  def self.register_ptype(loader, ir)
    create_ptype(loader, ir, 'AnyType',
                 'param_types' => {
                   KEY_TYPE => POptionalType.new(PTypeType.new(PTupleType::DEFAULT)),
                   KEY_VALUE => nil
                 },
                 'block_type' => {
                   KEY_TYPE => POptionalType.new(PTypeType.new(PCallableType::DEFAULT)),
                   KEY_VALUE => nil
                 },
                 'return_type' => {
                   KEY_TYPE => POptionalType.new(PTypeType::DEFAULT),
                   KEY_VALUE => PAnyType::DEFAULT
                 })
  end

  # @return [PAnyType] The type for the values returned by this callable. Returns `nil` if return value is unconstrained
  attr_reader :return_type

  # Types of parameters as a Tuple with required/optional count, or an Integer with min (required), max count
  # @return [PTupleType] the tuple representing the parameter types
  attr_reader :param_types

  # Although being an abstract type reference, only Callable, or all Callables wrapped in
  # Optional or Variant are supported
  # If not set, the meaning is that block is not supported.
  # @return [PAnyType|nil] the block type
  attr_reader :block_type

  # @param param_types [PTupleType]
  # @param block_type [PAnyType]
  # @param return_type [PAnyType]
  def initialize(param_types, block_type = nil, return_type = nil)
    @param_types = param_types
    @block_type = block_type
    @return_type = return_type == PAnyType::DEFAULT ? nil : return_type
  end

  def accept(visitor, guard)
    super
    @param_types.accept(visitor, guard) unless @param_types.nil?
    @block_type.accept(visitor, guard) unless @block_type.nil?
    @return_type.accept(visitor, guard) unless @return_type.nil?
  end

  def generalize
    if self == DEFAULT
      DEFAULT
    else
      params_t = @param_types.nil? ? nil : @param_types.generalize
      block_t = @block_type.nil? ? nil : @block_type.generalize
      return_t = @return_type.nil? ? nil : @return_type.generalize
      @param_types.equal?(params_t) && @block_type.equal?(block_t) && @return_type.equal?(return_t) ? self : PCallableType.new(params_t, block_t, return_t)
    end
  end

  def normalize(guard = nil)
    if self == DEFAULT
      DEFAULT
    else
      params_t = @param_types.nil? ? nil : @param_types.normalize(guard)
      block_t = @block_type.nil? ? nil : @block_type.normalize(guard)
      return_t = @return_type.nil? ? nil : @return_type.normalize(guard)
      @param_types.equal?(params_t) && @block_type.equal?(block_t) && @return_type.equal?(return_t) ? self : PCallableType.new(params_t, block_t, return_t)
    end
  end

  def instance?(o, guard = nil)
    (o.is_a?(Proc) || o.is_a?(Evaluator::Closure) || o.is_a?(Functions::Function)) && assignable?(TypeCalculator.infer(o), guard)
  end

  # Returns `true` if this instance is a callable that accepts the given _args_
  #
  # @param args [Array] the arguments to test
  # @return [Boolean] `true` if this instance is a callable that accepts the given _args_
  def callable_with?(args, block = nil)
    # nil param_types and compatible return type means other Callable is assignable
    return true if @param_types.nil?
    return false unless @param_types.instance?(args)

    if @block_type.nil?
      block.nil?
    else
      @block_type.instance?(block)
    end
  end

  # @api private
  def callable_args?(required_callable_t, guard)
    # If the required callable is equal or more specific than self, self is acceptable arguments
    required_callable_t.assignable?(self, guard)
  end

  def kind_of_callable?(optional = true, guard = nil)
    true
  end

  # Returns the number of accepted arguments [min, max]
  def size_range
    @param_types.nil? ? nil : @param_types.size_range
  end

  # Returns the number of accepted arguments for the last parameter type [min, max]
  #
  def last_range
    @param_types.nil? ? nil : @param_types.repeat_last_range
  end

  # Range [0,0], [0,1], or [1,1] for the block
  #
  def block_range
    case block_type
    when POptionalType
      [0, 1]
    when PVariantType, PCallableType
      [1, 1]
    else
      [0, 0]
    end
  end

  def hash
    [@param_types, @block_type, @return_type].hash
  end

  def eql?(o)
    self.class == o.class && @param_types == o.param_types && @block_type == o.block_type && @return_type == o.return_type
  end

  def resolve(loader)
    params_t = @param_types.nil? ? nil : @param_types.resolve(loader)
    block_t = @block_type.nil? ? nil : @block_type.resolve(loader)
    return_t = @return_type.nil? ? nil : @return_type.resolve(loader)
    @param_types.equal?(params_t) && @block_type.equal?(block_t) && @return_type.equal?(return_t) ? self : self.class.new(params_t, block_t, return_t)
  end

  DEFAULT = PCallableType.new(nil, nil, nil)

  protected

  # @api private
  def _assignable?(o, guard)
    return false unless o.is_a?(PCallableType)
    return false unless @return_type.nil? || @return_type.assignable?(o.return_type || PAnyType::DEFAULT, guard)

    # nil param_types and compatible return type means other Callable is assignable
    return true if @param_types.nil?

    # NOTE: these tests are made in reverse as it is calling the callable that is constrained
    # (it's lower bound), not its upper bound
    other_param_types = o.param_types

    return false if other_param_types.nil? || !other_param_types.assignable?(@param_types, guard)

    # names are ignored, they are just information
    # Blocks must be compatible
    this_block_t = @block_type || PUndefType::DEFAULT
    that_block_t = o.block_type || PUndefType::DEFAULT
    that_block_t.assignable?(this_block_t, guard)
  end
end

# @api public
#
class PArrayType < PCollectionType
  def self.register_ptype(loader, ir)
    create_ptype(loader, ir, 'CollectionType',
                 'element_type' => {
                   KEY_TYPE => POptionalType.new(PTypeType::DEFAULT),
                   KEY_VALUE => PAnyType::DEFAULT
                 })
  end

  attr_reader :element_type

  def initialize(element_type, size_type = nil)
    super(size_type)
    if !size_type.nil? && size_type.from == 0 && size_type.to == 0
      @element_type = PUnitType::DEFAULT
    else
      @element_type = element_type.nil? ? PAnyType::DEFAULT : element_type
    end
  end

  def accept(visitor, guard)
    super
    @element_type.accept(visitor, guard)
  end

  # @api private
  def callable_args?(callable, guard = nil)
    param_t = callable.param_types
    block_t = callable.block_type
    # does not support calling with a block, but have to check that callable is ok with missing block
    (param_t.nil? || param_t.assignable?(self, guard)) && (block_t.nil? || block_t.assignable?(PUndefType::DEFAULT, guard))
  end

  def generalize
    if PAnyType::DEFAULT.eql?(@element_type)
      DEFAULT
    else
      ge_type = @element_type.generalize
      @size_type.nil? && @element_type.equal?(ge_type) ? self : self.class.new(ge_type, nil)
    end
  end

  def eql?(o)
    super && @element_type == o.element_type
  end

  def hash
    super ^ @element_type.hash
  end

  def normalize(guard = nil)
    if PAnyType::DEFAULT.eql?(@element_type)
      DEFAULT
    else
      ne_type = @element_type.normalize(guard)
      @element_type.equal?(ne_type) ? self : self.class.new(ne_type, @size_type)
    end
  end

  def resolve(loader)
    relement_type = @element_type.resolve(loader)
    relement_type.equal?(@element_type) ? self : self.class.new(relement_type, @size_type)
  end

  def instance?(o, guard = nil)
    # The inferred type of a class derived from Array is either Runtime or Object. It's not assignable to the Array type.
    return false unless o.instance_of?(Array)
    return false unless o.all? { |element| @element_type.instance?(element, guard) }

    size_t = size_type
    size_t.nil? || size_t.instance?(o.size, guard)
  end

  def iterable_type(guard = nil)
    PAnyType::DEFAULT.eql?(@element_type) ? PIterableType::DEFAULT : PIterableType.new(@element_type)
  end

  # Returns a new function that produces an Array
  #
  def self.new_function(type)
    @new_function ||= Puppet::Functions.create_loaded_function(:new_array, type.loader) do
      dispatch :to_array do
        param           'Variant[Array,Hash,Binary,Iterable]', :from
        optional_param  'Boolean[false]', :wrap
      end

      dispatch :wrapped do
        param  'Any',           :from
        param  'Boolean[true]', :wrap
      end

      argument_mismatch :on_error do
        param  'Any',             :from
        optional_param 'Boolean', :wrap
      end

      def wrapped(from, _)
        from.is_a?(Array) ? from : [from]
      end

      def to_array(from, _ = false)
        case from
        when Array
          from
        when Hash
          from.to_a
        when PBinaryType::Binary
          # For older rubies, the #bytes method returns an Enumerator that must be rolled out
          from.binary_buffer.bytes.to_a
        else
          Iterable.on(from).to_a
        end
      end

      def on_error(from, _ = false)
        t = TypeCalculator.singleton.infer(from).generalize
        _("Value of type %{type} cannot be converted to Array") % { type: t }
      end
    end
  end

  DEFAULT = PArrayType.new(nil)
  EMPTY = PArrayType.new(PUnitType::DEFAULT, ZERO_SIZE)

  protected

  # Array is assignable if o is an Array and o's element type is assignable, or if o is a Tuple
  # @api private
  def _assignable?(o, guard)
    case o
    when PTupleType
      o_types = o.types
      size_s = size_type || DEFAULT_SIZE
      size_o = o.size_type
      if size_o.nil?
        type_count = o_types.size
        size_o = PIntegerType.new(type_count, type_count)
      end
      size_s.assignable?(size_o) && o_types.all? { |ot| @element_type.assignable?(ot, guard) }
    when PArrayType
      super && @element_type.assignable?(o.element_type, guard)
    else
      false
    end
  end
end

# @api public
#
class PHashType < PCollectionType
  def self.register_ptype(loader, ir)
    create_ptype(loader, ir, 'CollectionType',
                 'key_type' => {
                   KEY_TYPE => POptionalType.new(PTypeType::DEFAULT),
                   KEY_VALUE => PAnyType::DEFAULT
                 },
                 'value_type' => {
                   KEY_TYPE => POptionalType.new(PTypeType::DEFAULT),
                   KEY_VALUE => PAnyType::DEFAULT
                 })
  end

  attr_accessor :key_type, :value_type

  def initialize(key_type, value_type, size_type = nil)
    super(size_type)
    if !size_type.nil? && size_type.from == 0 && size_type.to == 0
      @key_type = PUnitType::DEFAULT
      @value_type = PUnitType::DEFAULT
    else
      @key_type = key_type.nil? ? PAnyType::DEFAULT : key_type
      @value_type = value_type.nil? ? PAnyType::DEFAULT : value_type
    end
  end

  def accept(visitor, guard)
    super
    @key_type.accept(visitor, guard)
    @value_type.accept(visitor, guard)
  end

  def element_type
    if Puppet[:strict] != :off
      # TRANSLATOR 'Puppet::Pops::Types::PHashType#element_type' and '#value_type' are class and method names and should not be translated
      Puppet.warn_once('deprecations', 'Puppet::Pops::Types::PHashType#element_type',
                       _('Puppet::Pops::Types::PHashType#element_type is deprecated, use #value_type instead'))
    end
    @value_type
  end

  def generalize
    if self == DEFAULT || self == EMPTY
      self
    else
      key_t = @key_type
      key_t = key_t.generalize
      value_t = @value_type
      value_t = value_t.generalize
      @size_type.nil? && @key_type.equal?(key_t) && @value_type.equal?(value_t) ? self : PHashType.new(key_t, value_t, nil)
    end
  end

  def normalize(guard = nil)
    if self == DEFAULT || self == EMPTY
      self
    else
      key_t = @key_type.normalize(guard)
      value_t = @value_type.normalize(guard)
      @size_type.nil? && @key_type.equal?(key_t) && @value_type.equal?(value_t) ? self : PHashType.new(key_t, value_t, @size_type)
    end
  end

  def hash
    super ^ @key_type.hash ^ @value_type.hash
  end

  def instance?(o, guard = nil)
    # The inferred type of a class derived from Hash is either Runtime or Object. It's not assignable to the Hash type.
    return false unless o.instance_of?(Hash)

    if o.keys.all? { |key| @key_type.instance?(key, guard) } && o.values.all? { |value| @value_type.instance?(value, guard) }
      size_t = size_type
      size_t.nil? || size_t.instance?(o.size, guard)
    else
      false
    end
  end

  def iterable?(guard = nil)
    true
  end

  def iterable_type(guard = nil)
    if self == DEFAULT || self == EMPTY
      PIterableType.new(DEFAULT_KEY_PAIR_TUPLE)
    else
      PIterableType.new(PTupleType.new([@key_type, @value_type], KEY_PAIR_TUPLE_SIZE))
    end
  end

  def eql?(o)
    super && @key_type == o.key_type && @value_type == o.value_type
  end

  def is_the_empty_hash?
    self == EMPTY
  end

  def resolve(loader)
    rkey_type = @key_type.resolve(loader)
    rvalue_type = @value_type.resolve(loader)
    rkey_type.equal?(@key_type) && rvalue_type.equal?(@value_type) ? self : self.class.new(rkey_type, rvalue_type, @size_type)
  end

  def self.array_as_hash(value)
    return value unless value.is_a?(Array)

    result = {}
    value.each_with_index { |v, idx| result[idx] = array_as_hash(v) }
    result
  end

  # Returns a new function that produces a  Hash
  #
  def self.new_function(type)
    @new_function ||= Puppet::Functions.create_loaded_function(:new_hash, type.loader) do
      local_types do
        type 'KeyValueArray = Array[Tuple[Any,Any],1]'
        type 'TreeArray = Array[Tuple[Array,Any],1]'
        type 'NewHashOption = Enum[tree, hash_tree]'
      end

      dispatch :from_tree do
        param           'TreeArray',       :from
        optional_param  'NewHashOption',   :build_option
      end

      dispatch :from_tuples do
        param           'KeyValueArray', :from
      end

      dispatch :from_array do
        param           'Any', :from
      end

      def from_tuples(tuple_array)
        tuple_array.to_h
      end

      def from_tree(tuple_array, build_option = nil)
        if build_option.nil?
          return from_tuples(tuple_array)
        end

        # only remaining possible options is 'tree' or 'hash_tree'

        all_hashes = build_option == 'hash_tree'
        result = {}
        tuple_array.each do |entry|
          path = entry[0]
          value = entry[1]
          if path.empty?
            # root node (index [] was included - values merge into the result)
            # An array must be changed to a hash first as this is the root
            # (Cannot return an array from a Hash.new)
            if value.is_a?(Array)
              value.each_with_index { |v, idx| result[idx] = v }
            else
              result.merge!(value)
            end
          else
            r = path[0..-2].reduce(result) { |memo, idx| (memo.is_a?(Array) || memo.has_key?(idx)) ? memo[idx] : memo[idx] = {} }
            r[path[-1]] = (all_hashes ? PHashType.array_as_hash(value) : value)
          end
        end
        result
      end

      def from_array(from)
        case from
        when Array
          if from.size == 0
            {}
          else
            unless from.size.even?
              raise TypeConversionError, _('odd number of arguments for Hash')
            end

            Hash[*from]
          end
        when Hash
          from
        else
          if PIterableType::DEFAULT.instance?(from)
            Hash[*Iterable.on(from).to_a]
          else
            t = TypeCalculator.singleton.infer(from).generalize
            raise TypeConversionError, _("Value of type %{type} cannot be converted to Hash") % { type: t }
          end
        end
      end
    end
  end

  DEFAULT = PHashType.new(nil, nil)
  KEY_PAIR_TUPLE_SIZE = PIntegerType.new(2, 2)
  DEFAULT_KEY_PAIR_TUPLE = PTupleType.new([PUnitType::DEFAULT, PUnitType::DEFAULT], KEY_PAIR_TUPLE_SIZE)
  EMPTY = PHashType.new(PUnitType::DEFAULT, PUnitType::DEFAULT, PIntegerType.new(0, 0))

  protected

  # Hash is assignable if o is a Hash and o's key and element types are assignable
  # @api private
  def _assignable?(o, guard)
    case o
    when PHashType
      size_s = size_type
      return true if (size_s.nil? || size_s.from == 0) && o.is_the_empty_hash?
      return false unless @key_type.assignable?(o.key_type, guard) && @value_type.assignable?(o.value_type, guard)

      super
    when PStructType
      # hash must accept String as key type
      # hash must accept all value types
      # hash must accept the size of the struct
      o_elements = o.elements
      (size_type || DEFAULT_SIZE).instance?(o_elements.size, guard) &&
        o_elements.all? { |e| @key_type.instance?(e.name, guard) && @value_type.assignable?(e.value_type, guard) }
    else
      false
    end
  end
end

# A flexible type describing an any? of other types
# @api public
#
class PVariantType < PAnyType
  include Enumerable

  def self.register_ptype(loader, ir)
    create_ptype(loader, ir, 'AnyType', 'types' => PArrayType.new(PTypeType::DEFAULT))
  end

  attr_reader :types

  # Checks if the number of unique types in the given array is greater than one, and if so
  # creates a Variant with those types and returns it. If only one unique type is found,
  # that type is instead returned.
  #
  # @param types [Array<PAnyType>] the variants
  # @return [PAnyType] the resulting type
  # @api public
  def self.maybe_create(types)
    types = flatten_variants(types).uniq
    types.size == 1 ? types[0] : new(types)
  end

  # @param types [Array[PAnyType]] the variants
  def initialize(types)
    @types = types.freeze
  end

  def accept(visitor, guard)
    super
    @types.each { |t| t.accept(visitor, guard) }
  end

  def each
    if block_given?
      types.each { |t| yield t }
    else
      types.to_enum
    end
  end

  def generalize
    if self == DEFAULT
      self
    else
      alter_type_array(@types, :generalize) { |altered| PVariantType.maybe_create(altered) }
    end
  end

  def normalize(guard = nil)
    if self == DEFAULT || @types.empty?
      self
    else
      # Normalize all contained types
      modified = false
      types = alter_type_array(@types, :normalize, guard)
      if types == self
        types = @types
      else
        modified = true
      end

      if types.size == 1
        types[0]
      elsif types.any? { |t| t.is_a?(PUndefType) || t.is_a?(POptionalType) }
        # Undef entry present. Use an OptionalType with a normalized Variant without Undefs and Optional wrappers
        POptionalType.new(PVariantType.maybe_create(types.reject { |t| t.is_a?(PUndefType) }.map { |t| t.is_a?(POptionalType) ? t.type : t })).normalize
      else
        # Merge all variants into this one
        types = PVariantType.flatten_variants(types)
        size_before_merge = types.size

        types = swap_not_undefs(types)
        types = merge_enums(types)
        types = merge_patterns(types)
        types = merge_version_ranges(types)
        types = merge_numbers(PIntegerType, types)
        types = merge_numbers(PFloatType, types)
        types = merge_numbers(PTimespanType, types)
        types = merge_numbers(PTimestampType, types)

        if types.size == 1
          types[0]
        else
          modified || types.size != size_before_merge ? PVariantType.maybe_create(types) : self
        end
      end
    end
  end

  def self.flatten_variants(types)
    modified = false
    types = types.map do |t|
      if t.is_a?(PVariantType)
        modified = true
        t.types
      else
        t
      end
    end
    types.flatten! if modified
    types
  end

  def hash
    @types.hash
  end

  def instance?(o, guard = nil)
    # instance of variant if o is instance? of any of variant's types
    @types.any? { |type| type.instance?(o, guard) }
  end

  def really_instance?(o, guard = nil)
    @types.reduce(-1) do |memo, type|
      ri = type.really_instance?(o, guard)
      break ri if ri > 0

      ri > memo ? ri : memo
    end
  end

  def kind_of_callable?(optional = true, guard = nil)
    @types.all? { |type| type.kind_of_callable?(optional, guard) }
  end

  def eql?(o)
    self.class == o.class && @types.size == o.types.size && (@types - o.types).empty?
  end

  DEFAULT = PVariantType.new(EMPTY_ARRAY)

  def assignable?(o, guard = nil)
    # an empty Variant does not match Undef (it is void - not even undef)
    if o.is_a?(PUndefType) && types.empty?
      return false
    end

    return super unless o.is_a?(PVariantType)
    # If empty, all Variant types match irrespective of the types they hold (including being empty)
    return true if types.empty?

    # Since this variant is not empty, an empty Variant cannot match, because it matches nothing
    # otherwise all types in o must be assignable to this
    !o.types.empty? && o.types.all? { |vt| super(vt, guard) }
  end

  protected

  # @api private
  def _assignable?(o, guard)
    # A variant is assignable if o is assignable to any of its types
    types.any? { |option_t| option_t.assignable?(o, guard) }
  end

  # @api private
  def swap_not_undefs(array)
    if array.size > 1
      parts = array.partition { |t| t.is_a?(PNotUndefType) }
      not_undefs = parts[0]
      if not_undefs.size > 1
        others = parts[1]
        others << PNotUndefType.new(PVariantType.maybe_create(not_undefs.map(&:type)).normalize)
        array = others
      end
    end
    array
  end

  # @api private
  def merge_enums(array)
    # Merge case sensitive enums and strings
    if array.size > 1
      parts = array.partition { |t| t.is_a?(PEnumType) && !t.values.empty? && !t.case_insensitive? || t.is_a?(PStringType) && !t.value.nil? }
      enums = parts[0]
      if enums.size > 1
        others = parts[1]
        others << PEnumType.new(enums.map { |enum| enum.is_a?(PStringType) ? enum.value : enum.values }.flatten.uniq)
        array = others
      end
    end

    # Merge case insensitive enums
    if array.size > 1
      parts = array.partition { |t| t.is_a?(PEnumType) && !t.values.empty? && t.case_insensitive? }
      enums = parts[0]
      if enums.size > 1
        others = parts[1]
        values = []
        enums.each { |enum| enum.values.each { |value| values << value.downcase } }
        values.uniq!
        others << PEnumType.new(values, true)
        array = others
      end
    end
    array
  end

  # @api private
  def merge_patterns(array)
    if array.size > 1
      parts = array.partition { |t| t.is_a?(PPatternType) }
      patterns = parts[0]
      if patterns.size > 1
        others = parts[1]
        others << PPatternType.new(patterns.map(&:patterns).flatten.uniq)
        array = others
      end
    end
    array
  end

  # @api private
  def merge_numbers(clazz, array)
    if array.size > 1
      parts = array.partition { |t| t.is_a?(clazz) }
      ranges = parts[0]
      array = merge_ranges(ranges) + parts[1] if ranges.size > 1
    end
    array
  end

  def merge_version_ranges(array)
    if array.size > 1
      parts = array.partition { |t| t.is_a?(PSemVerType) }
      ranges = parts[0]
      array = [PSemVerType.new(ranges.map(&:ranges).flatten)] + parts[1] if ranges.size > 1
    end
    array
  end

  # @api private
  def merge_ranges(ranges)
    result = []
    until ranges.empty?
      unmerged = []
      x = ranges.pop
      result << ranges.inject(x) do |memo, y|
        merged = memo.merge(y)
        if merged.nil?
          unmerged << y
        else
          memo = merged
        end
        memo
      end
      ranges = unmerged
    end
    result
  end
end

# Abstract representation of a type that can be placed in a Catalog.
# @api public
#
class PCatalogEntryType < PAnyType
  def self.register_ptype(loader, ir)
    create_ptype(loader, ir, 'AnyType')
  end

  DEFAULT = PCatalogEntryType.new

  def instance?(o, guard = nil)
    assignable?(TypeCalculator.infer(o), guard)
  end

  protected

  # @api private
  def _assignable?(o, guard)
    o.is_a?(PCatalogEntryType)
  end
end

# Represents a (host-) class in the Puppet Language.
# @api public
#
class PClassType < PCatalogEntryType
  attr_reader :class_name

  def self.register_ptype(loader, ir)
    create_ptype(loader, ir, 'CatalogEntryType',
                 'class_name' => {
                   KEY_TYPE => POptionalType.new(PStringType::NON_EMPTY),
                   KEY_VALUE => nil
                 })
  end

  def initialize(class_name)
    @class_name = class_name
  end

  def hash
    11 ^ @class_name.hash
  end

  def eql?(o)
    self.class == o.class && @class_name == o.class_name
  end

  DEFAULT = PClassType.new(nil)

  protected

  # @api private
  def _assignable?(o, guard)
    return false unless o.is_a?(PClassType)
    # Class = Class[name}, Class[name] != Class
    return true if @class_name.nil?

    # Class[name] = Class[name]
    @class_name == o.class_name
  end
end

# For backward compatibility
PHostClassType = PClassType

# Represents a Resource Type in the Puppet Language
# @api public
#
class PResourceType < PCatalogEntryType
  def self.register_ptype(loader, ir)
    create_ptype(loader, ir, 'CatalogEntryType',
                 'type_name' => {
                   KEY_TYPE => POptionalType.new(PStringType::NON_EMPTY),
                   KEY_VALUE => nil
                 },
                 'title' => {
                   KEY_TYPE => POptionalType.new(PStringType::NON_EMPTY),
                   KEY_VALUE => nil
                 })
  end

  attr_reader :type_name, :title, :downcased_name

  def initialize(type_name, title = nil)
    @type_name = type_name.freeze
    @title = title.freeze
    @downcased_name = type_name.nil? ? nil : @type_name.downcase.freeze
  end

  def eql?(o)
    self.class == o.class && @downcased_name == o.downcased_name && @title == o.title
  end

  def hash
    @downcased_name.hash ^ @title.hash
  end

  DEFAULT = PResourceType.new(nil)

  protected

  # @api private
  def _assignable?(o, guard)
    o.is_a?(PResourceType) && (@downcased_name.nil? || @downcased_name == o.downcased_name && (@title.nil? || @title == o.title))
  end
end

# Represents a type that accept PUndefType instead of the type parameter
# required_type - is a short hand for Variant[T, Undef]
# @api public
#
class POptionalType < PTypeWithContainedType
  def self.register_ptype(loader, ir)
    create_ptype(loader, ir, 'AnyType',
                 'type' => {
                   KEY_TYPE => POptionalType.new(PTypeType::DEFAULT),
                   KEY_VALUE => nil
                 })
  end

  def optional_type
    @type
  end

  def kind_of_callable?(optional = true, guard = nil)
    optional && !@type.nil? && @type.kind_of_callable?(optional, guard)
  end

  def instance?(o, guard = nil)
    PUndefType::DEFAULT.instance?(o, guard) || (!@type.nil? && @type.instance?(o, guard))
  end

  def normalize(guard = nil)
    n = super
    if n.type.nil?
      n
    elsif n.type.is_a?(PNotUndefType)
      POptionalType.new(n.type.type).normalize
    # No point in having an NotUndef in an Optional
    elsif n.type.assignable?(PUndefType::DEFAULT)
      # THe type is Optional anyway, so it can be stripped of
      n.type
    else
      n
    end
  end

  def new_function
    optional_type.new_function
  end

  DEFAULT = POptionalType.new(nil)

  protected

  # @api private
  def _assignable?(o, guard)
    return true if o.is_a?(PUndefType)
    return true if @type.nil?

    if o.is_a?(POptionalType)
      @type.assignable?(o.optional_type, guard)
    else
      @type.assignable?(o, guard)
    end
  end
end

class PTypeReferenceType < PAnyType
  def self.register_ptype(loader, ir)
    create_ptype(loader, ir, 'AnyType', 'type_string' => PStringType::NON_EMPTY)
  end

  attr_reader :type_string

  def initialize(type_string)
    @type_string = type_string
  end

  def callable?(args)
    false
  end

  def instance?(o, guard = nil)
    false
  end

  def hash
    @type_string.hash
  end

  def eql?(o)
    super && o.type_string == @type_string
  end

  def resolve(loader)
    TypeParser.singleton.parse(@type_string, loader)
  end

  protected

  def _assignable?(o, guard)
    # A type must be assignable to itself or a lot of unit tests will break
    o == self
  end

  DEFAULT = PTypeReferenceType.new('UnresolvedReference')
end

# Describes a named alias for another Type.
# The alias is created with a name and an unresolved type expression. The type expression may
# in turn contain other aliases (including the alias that contains it) which means that an alias
# might contain self recursion. Whether or not that is the case is computed and remembered when the alias
# is resolved since guarding against self recursive constructs is relatively expensive.
#
# @api public
class PTypeAliasType < PAnyType
  def self.register_ptype(loader, ir)
    create_ptype(loader, ir, 'AnyType',
                 'name' => PStringType::NON_EMPTY,
                 'type_expr' => PAnyType::DEFAULT,
                 'resolved_type' => {
                   KEY_TYPE => POptionalType.new(PTypeType::DEFAULT),
                   KEY_VALUE => nil
                 })
  end

  attr_reader :loader, :name

  # @param name [String] The name of the type
  # @param type_expr [Model::PopsObject] The expression that describes the aliased type
  # @param resolved_type [PAnyType] the resolve type (only used for the DEFAULT initialization)
  def initialize(name, type_expr, resolved_type = nil)
    @name = name
    @type_expr = type_expr
    @resolved_type = resolved_type
    @self_recursion = false
  end

  def assignable?(o, guard = nil)
    if @self_recursion
      guard ||= RecursionGuard.new
      guard.with_this(self) { |state| state == RecursionGuard::SELF_RECURSION_IN_BOTH ? true : super(o, guard) }
    else
      super(o, guard)
    end
  end

  # Returns the resolved type. The type must have been resolved by a call prior to calls to this
  # method or an error will be raised.
  #
  # @return [PAnyType] The resolved type of this alias.
  # @raise [Puppet::Error] unless the type has been resolved prior to calling this method
  def resolved_type
    raise Puppet::Error, "Reference to unresolved type #{@name}" unless @resolved_type

    @resolved_type
  end

  def callable_args?(callable, guard)
    guarded_recursion(guard, false) { |g| resolved_type.callable_args?(callable, g) }
  end

  def check_self_recursion(originator)
    resolved_type.check_self_recursion(originator) unless originator.equal?(self)
  end

  def kind_of_callable?(optional = true, guard = nil)
    guarded_recursion(guard, false) { |g| resolved_type.kind_of_callable?(optional, g) }
  end

  def instance?(o, guard = nil)
    really_instance?(o, guard) == 1
  end

  def iterable?(guard = nil)
    guarded_recursion(guard, false) { |g| resolved_type.iterable?(g) }
  end

  def iterable_type(guard = nil)
    guarded_recursion(guard, nil) { |g| resolved_type.iterable_type(g) }
  end

  def hash
    @name.hash
  end

  # Acceptor used when checking for self recursion and that a type contains
  # something other than aliases or type references
  #
  # @api private
  class AssertOtherTypeAcceptor
    def initialize
      @other_type_detected = false
    end

    def visit(type, _)
      unless type.is_a?(PTypeAliasType) || type.is_a?(PVariantType)
        @other_type_detected = true
      end
    end

    def other_type_detected?
      @other_type_detected
    end
  end

  # Acceptor used when re-checking for self recursion after a self recursion has been detected
  #
  # @api private
  class AssertSelfRecursionStatusAcceptor
    def visit(type, _)
      type.set_self_recursion_status if type.is_a?(PTypeAliasType)
    end
  end

  def set_self_recursion_status
    return if @self_recursion || @resolved_type.is_a?(PTypeReferenceType)

    @self_recursion = true
    guard = RecursionGuard.new
    accept(NoopTypeAcceptor::INSTANCE, guard)
    @self_recursion = guard.recursive_this?(self)
    when_self_recursion_detected if @self_recursion # no difference
  end

  # Called from the TypeParser once it has found a type using the Loader. The TypeParser will
  # interpret the contained expression and the resolved type is remembered. This method also
  # checks and remembers if the resolve type contains self recursion.
  #
  # @param type_parser [TypeParser] type parser that will interpret the type expression
  # @param loader [Loader::Loader] loader to use when loading type aliases
  # @return [PTypeAliasType] the receiver of the call, i.e. `self`
  # @api private
  def resolve(loader)
    @loader = loader
    if @resolved_type.nil?
      # resolved to PTypeReferenceType::DEFAULT during resolve to avoid endless recursion
      @resolved_type = PTypeReferenceType::DEFAULT
      @self_recursion = true # assumed while it being found out below
      begin
        if @type_expr.is_a?(PTypeReferenceType)
          @resolved_type = @type_expr.resolve(loader)
        else
          @resolved_type = TypeParser.singleton.interpret(@type_expr, loader).normalize
        end

        # Find out if this type is recursive. A recursive type has performance implications
        # on several methods and this knowledge is used to avoid that for non-recursive
        # types.
        guard = RecursionGuard.new
        real_type_asserter = AssertOtherTypeAcceptor.new
        accept(real_type_asserter, guard)
        unless real_type_asserter.other_type_detected?
          raise ArgumentError, "Type alias '#{name}' cannot be resolved to a real type"
        end

        @self_recursion = guard.recursive_this?(self)
        # All aliases involved must re-check status since this alias is now resolved
        if @self_recursion
          accept(AssertSelfRecursionStatusAcceptor.new, RecursionGuard.new)
          when_self_recursion_detected
        end
      rescue
        @resolved_type = nil
        raise
      end
    else
      # An alias may appoint an Object type that isn't resolved yet. The default type
      # reference is used to prevent endless recursion and should not be resolved here.
      @resolved_type.resolve(loader) unless @resolved_type.equal?(PTypeReferenceType::DEFAULT)
    end
    self
  end

  def eql?(o)
    super && o.name == @name
  end

  def accept(visitor, guard)
    guarded_recursion(guard, nil) do |g|
      super(visitor, g)
      @resolved_type.accept(visitor, g) unless @resolved_type.nil?
    end
  end

  def self_recursion?
    @self_recursion
  end

  # Returns the expanded string the form of the alias, e.g. <alias name> = <resolved type>
  #
  # @return [String] the expanded form of this alias
  # @api public
  def to_s
    TypeFormatter.singleton.alias_expanded_string(self)
  end

  # Delegates to resolved type
  def respond_to_missing?(name, include_private)
    resolved_type.respond_to?(name, include_private)
  end

  # Delegates to resolved type
  def method_missing(name, *arguments, &block)
    super if @resolved_type.equal?(PTypeReferenceType::DEFAULT)
    resolved_type.send(name, *arguments, &block)
  end

  # @api private
  def really_instance?(o, guard = nil)
    if @self_recursion
      guard ||= RecursionGuard.new
      guard.with_that(o) do
        guard.with_this(self) { |state| state == RecursionGuard::SELF_RECURSION_IN_BOTH ? 0 : resolved_type.really_instance?(o, guard) }
      end
    else
      resolved_type.really_instance?(o, guard)
    end
  end

  # @return `nil` to prevent serialization of the type_expr used when first initializing this instance
  # @api private
  def type_expr
    nil
  end

  protected

  def _assignable?(o, guard)
    resolved_type.assignable?(o, guard)
  end

  def new_function
    resolved_type.new_function
  end

  private

  def guarded_recursion(guard, dflt)
    if @self_recursion
      guard ||= RecursionGuard.new
      guard.with_this(self) { |state| (state & RecursionGuard::SELF_RECURSION_IN_THIS) == 0 ? yield(guard) : dflt }
    else
      yield(guard)
    end
  end

  def when_self_recursion_detected
    if @resolved_type.is_a?(PVariantType)
      # Drop variants that are not real types
      resolved_types = @resolved_type.types
      real_types = resolved_types.select do |type|
        next false if type == self

        real_type_asserter = AssertOtherTypeAcceptor.new
        type.accept(real_type_asserter, RecursionGuard.new)
        real_type_asserter.other_type_detected?
      end
      if real_types.size != resolved_types.size
        if real_types.size == 1
          @resolved_type = real_types[0]
        else
          @resolved_type = PVariantType.maybe_create(real_types)
        end
        # Drop self recursion status in case it's not self recursive anymore
        guard = RecursionGuard.new
        accept(NoopTypeAcceptor::INSTANCE, guard)
        @self_recursion = guard.recursive_this?(self)
      end
    end
    @resolved_type.check_self_recursion(self) if @self_recursion
  end

  DEFAULT = PTypeAliasType.new('UnresolvedAlias', nil, PTypeReferenceType::DEFAULT)
end
end
end

require_relative '../../../puppet/pops/pcore'

require_relative 'annotatable'
require_relative 'p_meta_type'
require_relative 'p_object_type'
require_relative 'annotation'
require_relative 'ruby_method'
require_relative 'p_runtime_type'
require_relative 'p_sem_ver_type'
require_relative 'p_sem_ver_range_type'
require_relative 'p_sensitive_type'
require_relative 'p_type_set_type'
require_relative 'p_timespan_type'
require_relative 'p_timestamp_type'
require_relative 'p_binary_type'
require_relative 'p_init_type'
require_relative 'p_object_type_extension'
require_relative 'p_uri_type'
require_relative 'type_set_reference'
require_relative 'implementation_registry'
require_relative 'tree_iterators'
