# frozen_string_literal: true

# Determines whether a given class or resource type is defined and returns a Boolean
# value. You can also use `defined` to determine whether a specific resource is defined,
# or whether a variable has a value (including `undef`, as opposed to the variable never
# being declared or assigned).
#
# This function takes at least one string argument, which can be a class name, type name,
# resource reference, or variable reference of the form `'$name'`. (Note that the `$` sign
# is included in the string which must be in single quotes to prevent the `$` character
# to be interpreted as interpolation.
#
# The `defined` function checks both native and defined types, including types
# provided by modules. Types and classes are matched by their names. The function matches
# resource declarations by using resource references.
#
# @example Different types of `defined` function matches
#
# ```puppet
# # Matching resource types
# defined("file")
# defined("customtype")
#
# # Matching defines and classes
# defined("foo")
# defined("foo::bar")
#
# # Matching variables (note the single quotes)
# defined('$name')
#
# # Matching declared resources
# defined(File['/tmp/file'])
# ```
#
# Puppet depends on the configuration's evaluation order when checking whether a resource
# is declared.
#
# @example Importance of evaluation order when using `defined`
#
# ```puppet
# # Assign values to $is_defined_before and $is_defined_after using identical `defined`
# # functions.
#
# $is_defined_before = defined(File['/tmp/file'])
#
# file { "/tmp/file":
#   ensure => present,
# }
#
# $is_defined_after = defined(File['/tmp/file'])
#
# # $is_defined_before returns false, but $is_defined_after returns true.
# ```
#
# This order requirement only refers to evaluation order. The order of resources in the
# configuration graph (e.g. with `before` or `require`) does not affect the `defined`
# function's behavior.
#
# > **Warning:** Avoid relying on the result of the `defined` function in modules, as you
# > might not be able to guarantee the evaluation order well enough to produce consistent
# > results. This can cause other code that relies on the function's result to behave
# > inconsistently or fail.
#
# If you pass more than one argument to `defined`, the function returns `true` if _any_
# of the arguments are defined. You can also match resources by type, allowing you to
# match conditions of different levels of specificity, such as whether a specific resource
# is of a specific data type.
#
# @example Matching multiple resources and resources by different types with `defined`
#
# ```puppet
# file { "/tmp/file1":
#   ensure => file,
# }
#
# $tmp_file = file { "/tmp/file2":
#   ensure => file,
# }
#
# # Each of these statements return `true` ...
# defined(File['/tmp/file1'])
# defined(File['/tmp/file1'],File['/tmp/file2'])
# defined(File['/tmp/file1'],File['/tmp/file2'],File['/tmp/file3'])
# # ... but this returns `false`.
# defined(File['/tmp/file3'])
#
# # Each of these statements returns `true` ...
# defined(Type[Resource['file','/tmp/file2']])
# defined(Resource['file','/tmp/file2'])
# defined(File['/tmp/file2'])
# defined('$tmp_file')
# # ... but each of these returns `false`.
# defined(Type[Resource['exec','/tmp/file2']])
# defined(Resource['exec','/tmp/file2'])
# defined(File['/tmp/file3'])
# defined('$tmp_file2')
# ```
#
# @since 2.7.0
# @since 3.6.0 variable reference and future parser types
# @since 3.8.1 type specific requests with future parser
# @since 4.0.0
#
Puppet::Functions.create_function(:defined, Puppet::Functions::InternalFunction) do
  dispatch :is_defined do
    scope_param
    required_repeated_param 'Variant[String, Type[CatalogEntry], Type[Type[CatalogEntry]]]', :vals
  end

  def is_defined(scope, *vals) # rubocop:disable Naming/PredicatePrefix
    vals.any? do |val|
      case val
      when String
        if val =~ /^\$(.+)$/
          scope.exist?(Regexp.last_match(1))
        else
          case val
          when ''
            next nil
          when 'main'
            # Find the main class (known as ''), it does not have to be in the catalog
            Puppet::Pops::Evaluator::Runtime3ResourceSupport.find_main_class(scope)
          else
            # Find a resource type, definition or class definition
            Puppet::Pops::Evaluator::Runtime3ResourceSupport.find_resource_type_or_class(scope, val)
          end
        end
      when Puppet::Resource
        # Find instance of given resource type and title that is in the catalog
        scope.compiler.findresource(val.resource_type, val.title)

      when Puppet::Pops::Types::PResourceType
        raise ArgumentError, _('The given resource type is a reference to all kind of types') if val.type_name.nil?

        type = Puppet::Pops::Evaluator::Runtime3ResourceSupport.find_resource_type(scope, val.type_name)
        val.title.nil? ? type : scope.compiler.findresource(type, val.title)

      when Puppet::Pops::Types::PClassType
        raise ArgumentError, _('The given class type is a reference to all classes') if val.class_name.nil?

        scope.compiler.findresource(:class, val.class_name)

      when Puppet::Pops::Types::PTypeType
        case val.type
        when Puppet::Pops::Types::PResourceType
          # It is most reasonable to take Type[File] and Type[File[foo]] to mean the same as if not wrapped in a Type
          # Since the difference between File and File[foo] already captures the distinction of type vs instance.
          is_defined(scope, val.type)

        when Puppet::Pops::Types::PClassType
          # Interpreted as asking if a class (and nothing else) is defined without having to be included in the catalog
          # (this is the same as asking for just the class' name, but with the added certainty that it cannot be a defined type.
          #
          raise ArgumentError, _('The given class type is a reference to all classes') if val.type.class_name.nil?

          Puppet::Pops::Evaluator::Runtime3ResourceSupport.find_hostclass(scope, val.type.class_name)
        end
      else
        raise ArgumentError, _("Invalid argument of type '%{value_class}' to 'defined'") % { value_class: val.class }
      end
    end
  end
end
