# frozen_string_literal: true

module Puppet::Pops
module Types
# @api public
class PRuntimeType < PAnyType
  TYPE_NAME_OR_PATTERN = PVariantType.new([PStringType::NON_EMPTY, PTupleType.new([PRegexpType::DEFAULT, PStringType::NON_EMPTY])])

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

  attr_reader :runtime, :name_or_pattern

  # Creates a new instance of a Runtime type
  #
  # @param runtime [String] the name of the runtime, e.g. 'ruby'
  # @param name_or_pattern [String,Array(Regexp,String)] name of runtime or two patterns, mapping Puppet name => runtime name
  # @api public
  def initialize(runtime, name_or_pattern)
    unless runtime.nil? || runtime.is_a?(Symbol)
      runtime = TypeAsserter.assert_instance_of("Runtime 'runtime'", PStringType::NON_EMPTY, runtime).to_sym
    end
    @runtime = runtime
    @name_or_pattern = TypeAsserter.assert_instance_of("Runtime 'name_or_pattern'", TYPE_NAME_OR_PATTERN, name_or_pattern, true)
  end

  def hash
    @runtime.hash ^ @name_or_pattern.hash
  end

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

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

  def iterable?(guard = nil)
    if @runtime == :ruby && !runtime_type_name.nil?
      begin
        c = ClassLoader.provide(self)
        return c < Iterable unless c.nil?
      rescue ArgumentError
      end
    end
    false
  end

  def iterable_type(guard = nil)
    iterable?(guard) ? PIterableType.new(self) : nil
  end

  # @api private
  def runtime_type_name
    @name_or_pattern.is_a?(String) ? @name_or_pattern : nil
  end

  # @api private
  def class_or_module
    raise "Only ruby classes or modules can be produced by this runtime, got '#{runtime}" unless runtime == :ruby
    raise 'A pattern based Runtime type cannot produce a class or module' if @name_or_pattern.is_a?(Array)

    com = ClassLoader.provide(self)
    raise "The name #{@name_or_pattern} does not represent a ruby class or module" if com.nil?

    com
  end

  # @api private
  def from_puppet_name(puppet_name)
    if @name_or_pattern.is_a?(Array)
      substituted = puppet_name.sub(*@name_or_pattern)
      substituted == puppet_name ? nil : PRuntimeType.new(@runtime, substituted)
    else
      nil
    end
  end

  DEFAULT = PRuntimeType.new(nil, nil)
  RUBY = PRuntimeType.new(:ruby, nil)

  protected

  # Assignable if o's has the same runtime and the runtime name resolves to
  # a class that is the same or subclass of t1's resolved runtime type name
  # @api private
  def _assignable?(o, guard)
    return false unless o.is_a?(PRuntimeType)
    return false unless @runtime.nil? || @runtime == o.runtime
    return true if @name_or_pattern.nil? # t1 is wider

    onp = o.name_or_pattern
    return true if @name_or_pattern == onp
    return false unless @name_or_pattern.is_a?(String) && onp.is_a?(String)

    # NOTE: This only supports Ruby, must change when/if the set of runtimes is expanded
    begin
      c1 = ClassLoader.provide(self)
      c2 = ClassLoader.provide(o)
      c1.is_a?(Module) && c2.is_a?(Module) && !!(c2 <= c1)
    rescue ArgumentError
      false
    end
  end
end
end
end
