# frozen_string_literal: true

# The scope class, which handles storing and retrieving variables and types and
# such.
require 'forwardable'

require_relative '../../puppet/parser'
require_relative '../../puppet/parser/templatewrapper'
require_relative '../../puppet/parser/resource'

# This class is part of the internal parser/evaluator/compiler functionality of Puppet.
# It is passed between the various classes that participate in evaluation.
# None of its methods are API except those that are clearly marked as such.
#
# @api public
class Puppet::Parser::Scope
  extend Forwardable

  # Variables that always exist with nil value even if not set
  BUILT_IN_VARS = %w[module_name caller_module_name].freeze
  EMPTY_HASH = {}.freeze

  Puppet::Util.logmethods(self)

  include Puppet::Util::Errors
  attr_accessor :source, :resource
  attr_reader :compiler
  attr_accessor :parent

  # Hash of hashes of default values per type name
  attr_reader :defaults

  # Alias for `compiler.environment`
  def environment
    @compiler.environment
  end

  # Alias for `compiler.catalog`
  def catalog
    @compiler.catalog
  end

  # Abstract base class for LocalScope and MatchScope
  #
  class Ephemeral
    attr_reader :parent

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

    def is_local_scope?
      false
    end

    def [](name)
      if @parent
        @parent[name]
      end
    end

    def include?(name)
      (@parent and @parent.include?(name))
    end

    def bound?(name)
      false
    end

    def add_entries_to(target = {}, include_undef = false)
      @parent.add_entries_to(target, include_undef) unless @parent.nil?
      # do not include match data ($0-$n)
      target
    end
  end

  class LocalScope < Ephemeral
    def initialize(parent = nil)
      super parent
      @symbols = {}
    end

    def [](name)
      val = @symbols[name]
      val.nil? && !@symbols.include?(name) ? super : val
    end

    def is_local_scope?
      true
    end

    def []=(name, value)
      @symbols[name] = value
    end

    def include?(name)
      bound?(name) || super
    end

    def delete(name)
      @symbols.delete(name)
    end

    def bound?(name)
      @symbols.include?(name)
    end

    def add_entries_to(target = {}, include_undef = false)
      super
      @symbols.each do |k, v|
        if (v == :undef || v.nil?) && !include_undef
          target.delete(k)
        else
          target[k] = v
        end
      end
      target
    end
  end

  class MatchScope < Ephemeral
    attr_accessor :match_data

    def initialize(parent = nil, match_data = nil)
      super parent
      @match_data = match_data
    end

    def is_local_scope?
      false
    end

    def [](name)
      if bound?(name)
        @match_data[name.to_i]
      else
        super
      end
    end

    def include?(name)
      bound?(name) or super
    end

    def bound?(name)
      # A "match variables" scope reports all numeric variables to be bound if the scope has
      # match_data. Without match data the scope is transparent.
      #
      @match_data && name =~ /^\d+$/
    end

    def []=(name, value)
      # TODO: Bad choice of exception
      raise Puppet::ParseError, _("Numerical variables cannot be changed. Attempt to set $%{name}") % { name: name }
    end

    def delete(name)
      # TODO: Bad choice of exception
      raise Puppet::ParseError, _("Numerical variables cannot be deleted: Attempt to delete: $%{name}") % { name: name }
    end

    def add_entries_to(target = {}, include_undef = false)
      # do not include match data ($0-$n)
      super
    end
  end

  # @api private
  class ParameterScope < Ephemeral
    class Access
      attr_accessor :value

      def assigned?
        instance_variable_defined?(:@value)
      end
    end

    # A parameter default must be evaluated using a special scope. The scope that is given to this method must
    # have a `ParameterScope` as its last ephemeral scope. This method will then push a `MatchScope` while the
    # given `expression` is evaluated. The method will catch any throw of `:unevaluated_parameter` and produce
    # an error saying that the evaluated parameter X tries to access the unevaluated parameter Y.
    #
    # @param name [String] the name of the currently evaluated parameter
    # @param expression [Puppet::Parser::AST] the expression to evaluate
    # @param scope [Puppet::Parser::Scope] a scope where a `ParameterScope` has been pushed
    # @return [Object] the result of the evaluation
    #
    # @api private
    def evaluate3x(name, expression, scope)
      scope.with_guarded_scope do
        bad = catch(:unevaluated_parameter) do
          scope.new_match_scope(nil)
          return as_read_only { expression.safeevaluate(scope) }
        end
        parameter_reference_failure(name, bad)
      end
    end

    def evaluate(name, expression, scope, evaluator)
      scope.with_guarded_scope do
        bad = catch(:unevaluated_parameter) do
          scope.new_match_scope(nil)
          return as_read_only { evaluator.evaluate(expression, scope) }
        end
        parameter_reference_failure(name, bad)
      end
    end

    def parameter_reference_failure(from, to)
      # Parameters are evaluated in the order they have in the @params hash.
      keys = @params.keys
      raise Puppet::Error, _("%{callee}: expects a value for parameter $%{to}") % { callee: @callee_name, to: to } if keys.index(to) < keys.index(from)

      raise Puppet::Error, _("%{callee}: default expression for $%{from} tries to illegally access not yet evaluated $%{to}") % { callee: @callee_name, from: from, to: to }
    end
    private :parameter_reference_failure

    def initialize(parent, callee_name, param_names)
      super(parent)
      @callee_name = callee_name
      @params = {}
      param_names.each { |name| @params[name] = Access.new }
    end

    def [](name)
      access = @params[name]
      return super if access.nil?

      throw(:unevaluated_parameter, name) unless access.assigned?
      access.value
    end

    def []=(name, value)
      raise Puppet::Error, _("Attempt to assign variable %{name} when evaluating parameters") % { name: name } if @read_only

      @params[name] ||= Access.new
      @params[name].value = value
    end

    def bound?(name)
      @params.include?(name)
    end

    def include?(name)
      @params.include?(name) || super
    end

    def is_local_scope?
      true
    end

    def as_read_only
      read_only = @read_only
      @read_only = true
      begin
        yield
      ensure
        @read_only = read_only
      end
    end

    def to_hash
      @params.select { |_, access| access.assigned? }.transform_values(&:value)
    end
  end

  # Returns true if the variable of the given name has a non nil value.
  # TODO: This has vague semantics - does the variable exist or not?
  #       use ['name'] to get nil or value, and if nil check with exist?('name')
  #       this include? is only useful because of checking against the boolean value false.
  #
  def include?(name)
    catch(:undefined_variable) {
      return !self[name].nil?
    }
    false
  end

  # Returns true if the variable of the given name is set to any value (including nil)
  #
  # @return [Boolean] if variable exists or not
  #
  def exist?(name)
    # Note !! ensure the answer is boolean
    !!if name =~ /^(.*)::(.+)$/
        class_name = ::Regexp.last_match(1)
        variable_name = ::Regexp.last_match(2)
        return true if class_name == '' && BUILT_IN_VARS.include?(variable_name)

        # lookup class, but do not care if it is not evaluated since that will result
        # in it not existing anyway. (Tests may run with just scopes and no evaluated classes which
        # will result in class_scope for "" not returning topscope).
        klass = find_hostclass(class_name)
        other_scope = klass.nil? ? nil : class_scope(klass)
        if other_scope.nil?
          class_name == '' ? compiler.topscope.exist?(variable_name) : false
        else
          other_scope.exist?(variable_name)
        end
    else # rubocop:disable Layout/ElseAlignment
      next_scope = inherited_scope || enclosing_scope
      effective_symtable(true).include?(name) || next_scope && next_scope.exist?(name) || BUILT_IN_VARS.include?(name)
    end # rubocop:disable Layout/EndAlignment
  end

  # Returns true if the given name is bound in the current (most nested) scope for assignments.
  #
  def bound?(name)
    # Do not look in ephemeral (match scope), the semantics is to answer if an assignable variable is bound
    effective_symtable(false).bound?(name)
  end

  # Is the value true?  This allows us to control the definition of truth
  # in one place.
  def self.true?(value)
    case value
    when ''
      false
    when :undef
      false
    else
      !!value
    end
  end

  # Coerce value to a number, or return `nil` if it isn't one.
  def self.number?(value)
    case value
    when Numeric
      value
    when /^-?\d+(:?\.\d+|(:?\.\d+)?e\d+)$/
      value.to_f
    when /^0x[0-9a-f]+$/i
      value.to_i(16)
    when /^0[0-7]+$/
      value.to_i(8)
    when /^-?\d+$/
      value.to_i
    else
      nil
    end
  end

  def find_hostclass(name)
    environment.known_resource_types.find_hostclass(name)
  end

  def find_definition(name)
    environment.known_resource_types.find_definition(name)
  end

  def find_global_scope
    # walk upwards until first found node_scope or top_scope
    if is_nodescope? || is_topscope?
      self
    else
      next_scope = inherited_scope || enclosing_scope
      if next_scope.nil?
        # this happens when testing, and there is only a single test scope and no link to any
        # other scopes
        self
      else
        next_scope.find_global_scope()
      end
    end
  end

  def findresource(type, title = nil)
    @compiler.catalog.resource(type, title)
  end

  # Initialize our new scope.  Defaults to having no parent.
  def initialize(compiler, source: nil, resource: nil)
    if compiler.is_a? Puppet::Parser::AbstractCompiler
      @compiler = compiler
    else
      raise Puppet::DevError, _("you must pass a compiler instance to a new scope object")
    end

    @source = source
    @resource = resource

    extend_with_functions_module

    # The symbol table for this scope.  This is where we store variables.
    #    @symtable = Ephemeral.new(nil, true)
    @symtable = LocalScope.new(nil)

    @ephemeral = [MatchScope.new(@symtable, nil)]

    # All of the defaults set for types.  It's a hash of hashes,
    # with the first key being the type, then the second key being
    # the parameter.
    @defaults = Hash.new { |dhash, type|
      dhash[type] = {}
    }

    # The table for storing class singletons.  This will only actually
    # be used by top scopes and node scopes.
    @class_scopes = {}
  end

  # Store the fact that we've evaluated a class, and store a reference to
  # the scope in which it was evaluated, so that we can look it up later.
  def class_set(name, scope)
    if parent
      parent.class_set(name, scope)
    else
      @class_scopes[name] = scope
    end
  end

  # Return the scope associated with a class.  This is just here so
  # that subclasses can set their parent scopes to be the scope of
  # their parent class, and it's also used when looking up qualified
  # variables.
  def class_scope(klass)
    # They might pass in either the class or class name
    k = klass.respond_to?(:name) ? klass.name : klass
    @class_scopes[k] || (parent && parent.class_scope(k))
  end

  # Collect all of the defaults set at any higher scopes.
  # This is a different type of lookup because it's
  # additive -- it collects all of the defaults, with defaults
  # in closer scopes overriding those in later scopes.
  #
  # The lookupdefaults searches in the order:
  #
  #   * inherited
  #   * contained (recursive)
  #   * self
  #
  def lookupdefaults(type)
    values = {}

    # first collect the values from the parents
    if parent
      parent.lookupdefaults(type).each { |var, value|
        values[var] = value
      }
    end

    # then override them with any current values
    # this should probably be done differently
    if @defaults.include?(type)
      @defaults[type].each { |var, value|
        values[var] = value
      }
    end

    values
  end

  # Check if the given value is a known default for the given type
  #
  def is_default?(type, key, value)
    defaults_for_type = @defaults[type]
    unless defaults_for_type.nil?
      default_param = defaults_for_type[key]
      return true if !default_param.nil? && value == default_param.value
    end
    !parent.nil? && parent.is_default?(type, key, value)
  end

  # Look up a defined type.
  def lookuptype(name)
    # This happens a lot, avoid making a call to make a call
    krt = environment.known_resource_types
    krt.find_definition(name) || krt.find_hostclass(name)
  end

  def undef_as(x, v)
    if v.nil? or v == :undef
      x
    else
      v
    end
  end

  # Lookup a variable within this scope using the Puppet language's
  # scoping rules. Variables can be qualified using just as in a
  # manifest.
  #
  # @param [String] name the variable name to lookup
  # @param [Hash] hash of options, only internal code should give this
  # @param [Boolean] if resolution is of the leaf of a qualified name - only internal code should give this
  # @return Object the value of the variable, or if not found; nil if `strict_variables` is false, and thrown :undefined_variable otherwise
  #
  # @api public
  def lookupvar(name, options = EMPTY_HASH)
    unless name.is_a? String
      raise Puppet::ParseError, _("Scope variable name %{name} is a %{klass}, not a string") % { name: name.inspect, klass: name.class }
    end

    # If name has '::' in it, it is resolved as a qualified variable
    unless (idx = name.index('::')).nil?
      # Always drop leading '::' if present as that is how the values are keyed.
      return lookup_qualified_variable(idx == 0 ? name[2..] : name, options)
    end

    # At this point, search is for a non qualified (simple) name
    table = @ephemeral.last
    val = table[name]
    return val unless val.nil? && !table.include?(name)

    next_scope = inherited_scope || enclosing_scope
    if next_scope
      next_scope.lookupvar(name, options)
    else
      variable_not_found(name)
    end
  end

  UNDEFINED_VARIABLES_KIND = 'undefined_variables'

  # The exception raised when a throw is uncaught is different in different versions
  # of ruby. In >=2.2.0 it is UncaughtThrowError (which did not exist prior to this)
  #
  UNCAUGHT_THROW_EXCEPTION = defined?(UncaughtThrowError) ? UncaughtThrowError : ArgumentError

  def variable_not_found(name, reason = nil)
    # Built in variables and numeric variables always exist
    if BUILT_IN_VARS.include?(name) || name =~ Puppet::Pops::Patterns::NUMERIC_VAR_NAME
      return nil
    end

    begin
      throw(:undefined_variable, reason)
    rescue UNCAUGHT_THROW_EXCEPTION
      case Puppet[:strict]
      when :off
        # do nothing
      when :warning
        Puppet.warn_once(UNDEFINED_VARIABLES_KIND, _("Variable: %{name}") % { name: name },
                         _("Undefined variable '%{name}'; %{reason}") % { name: name, reason: reason })
      when :error
        if Puppet.lookup(:avoid_hiera_interpolation_errors) { false }
          Puppet.warn_once(UNDEFINED_VARIABLES_KIND, _("Variable: %{name}") % { name: name },
                           _("Interpolation failed with '%{name}', but compilation continuing; %{reason}") % { name: name, reason: reason })
        else
          raise ArgumentError, _("Undefined variable '%{name}'; %{reason}") % { name: name, reason: reason }
        end
      end
    end
    nil
  end

  # Retrieves the variable value assigned to the name given as an argument. The name must be a String,
  # and namespace can be qualified with '::'. The value is looked up in this scope, its parent scopes,
  # or in a specific visible named scope.
  #
  # @param varname [String] the name of the variable (may be a qualified name using `(ns'::')*varname`
  # @param options [Hash] Additional options, not part of api.
  # @return [Object] the value assigned to the given varname
  # @see #[]=
  # @api public
  #
  def [](varname, options = EMPTY_HASH)
    lookupvar(varname, options)
  end

  # The class scope of the inherited thing of this scope's resource.
  #
  # @return [Puppet::Parser::Scope] The scope or nil if there is not an inherited scope
  def inherited_scope
    if resource && resource.type == TYPENAME_CLASS && !resource.resource_type.parent.nil?
      qualified_scope(resource.resource_type.parent)
    else
      nil
    end
  end

  # The enclosing scope (topscope or nodescope) of this scope.
  # The enclosing scopes are produced when a class or define is included at
  # some point. The parent scope of the included class or define becomes the
  # scope in which it was included. The chain of parent scopes is followed
  # until a node scope or the topscope is found
  #
  # @return [Puppet::Parser::Scope] The scope or nil if there is no enclosing scope
  def enclosing_scope
    if has_enclosing_scope?
      if parent.is_topscope? || parent.is_nodescope?
        parent
      else
        parent.enclosing_scope
      end
    end
  end

  def is_classscope?
    resource && resource.type == TYPENAME_CLASS
  end

  def is_nodescope?
    resource && resource.type == TYPENAME_NODE
  end

  def is_topscope?
    equal?(@compiler.topscope)
  end

  # @api private
  def lookup_qualified_variable(fqn, options)
    table = @compiler.qualified_variables
    val = table[fqn]
    return val if !val.nil? || table.include?(fqn)

    # not found - search inherited scope for class
    leaf_index = fqn.rindex('::')
    unless leaf_index.nil?
      leaf_name = fqn[(leaf_index + 2)..]
      class_name = fqn[0, leaf_index]
      begin
        qs = qualified_scope(class_name)
        unless qs.nil?
          return qs.get_local_variable(leaf_name) if qs.has_local_variable?(leaf_name)

          iscope = qs.inherited_scope
          return lookup_qualified_variable("#{iscope.source.name}::#{leaf_name}", options) unless iscope.nil?
        end
      rescue RuntimeError => e
        # because a failure to find the class, or inherited should be reported against given name
        return handle_not_found(class_name, leaf_name, options, e.message)
      end
    end
    # report with leading '::' by using empty class_name
    handle_not_found('', fqn, options)
  end

  # @api private
  def has_local_variable?(name)
    @ephemeral.last.include?(name)
  end

  # @api private
  def get_local_variable(name)
    @ephemeral.last[name]
  end

  def handle_not_found(class_name, variable_name, position, reason = nil)
    unless Puppet[:strict_variables]
      # Do not issue warning if strict variables are on, as an error will be raised by variable_not_found
      location = if position[:lineproc]
                   Puppet::Util::Errors.error_location_with_space(nil, position[:lineproc].call)
                 else
                   Puppet::Util::Errors.error_location_with_space(position[:file], position[:line])
                 end
      variable_not_found("#{class_name}::#{variable_name}", "#{reason}#{location}")
      return nil
    end
    variable_not_found("#{class_name}::#{variable_name}", reason)
  end

  def has_enclosing_scope?
    !parent.nil?
  end
  private :has_enclosing_scope?

  def qualified_scope(classname)
    klass = find_hostclass(classname)
    raise _("class %{classname} could not be found") % { classname: classname } unless klass

    kscope = class_scope(klass)
    raise _("class %{classname} has not been evaluated") % { classname: classname } unless kscope

    kscope
  end
  private :qualified_scope

  # Returns a Hash containing all variables and their values, optionally (and
  # by default) including the values defined in parent. Local values
  # shadow parent values. Ephemeral scopes for match results ($0 - $n) are not included.
  # Optionally include the variables that are explicitly set to `undef`.
  #
  def to_hash(recursive = true, include_undef = false)
    if recursive and has_enclosing_scope?
      target = enclosing_scope.to_hash(recursive)
      unless (inherited = inherited_scope).nil?
        target.merge!(inherited.to_hash(recursive))
      end
    else
      target = Hash.new
    end

    # add all local scopes
    @ephemeral.last.add_entries_to(target, include_undef)
    target
  end

  # Create a new scope and set these options.
  def newscope(options = {})
    compiler.newscope(self, options)
  end

  def parent_module_name
    return nil unless @parent && @parent.source

    @parent.source.module_name
  end

  # Set defaults for a type.  The typename should already be downcased,
  # so that the syntax is isolated.  We don't do any kind of type-checking
  # here; instead we let the resource do it when the defaults are used.
  def define_settings(type, params)
    table = @defaults[type]

    # if we got a single param, it'll be in its own array
    params = [params] unless params.is_a?(Array)

    params.each { |param|
      if table.include?(param.name)
        raise Puppet::ParseError.new(_("Default already defined for %{type} { %{param} }; cannot redefine") % { type: type, param: param.name }, param.file, param.line)
      end

      table[param.name] = param
    }
  end

  # Merge all settings for the given _env_name_ into this scope
  # @param env_name [Symbol] the name of the environment
  # @param set_in_this_scope [Boolean] if the settings variables should also be set in this instance of scope
  def merge_settings(env_name, set_in_this_scope = true)
    settings = Puppet.settings
    table = effective_symtable(false)
    global_table = compiler.qualified_variables
    all_local = {}
    settings.each_key do |name|
      next if :name == name

      key = name.to_s
      value = transform_setting(settings.value_sym(name, env_name))
      if set_in_this_scope
        table[key] = value
      end
      all_local[key] = value
      # also write the fqn into global table for direct lookup
      global_table["settings::#{key}"] = value
    end
    # set the 'all_local' - a hash of all settings
    global_table["settings::all_local"] = all_local
    nil
  end

  def transform_setting(val)
    case val
    when String, Numeric, true, false, nil
      val
    when Array
      val.map { |entry| transform_setting(entry) }
    when Hash
      result = {}
      val.each { |k, v| result[transform_setting(k)] = transform_setting(v) }
      result
    else
      # not ideal, but required as there are settings values that are special
      :undef == val ? nil : val.to_s
    end
  end
  private :transform_setting

  VARNAME_TRUSTED = 'trusted'
  VARNAME_FACTS = 'facts'
  VARNAME_SERVER_FACTS = 'server_facts'
  RESERVED_VARIABLE_NAMES = [VARNAME_TRUSTED, VARNAME_FACTS].freeze
  TYPENAME_CLASS = 'Class'
  TYPENAME_NODE = 'Node'

  # Set a variable in the current scope.  This will override settings
  # in scopes above, but will not allow variables in the current scope
  # to be reassigned.
  #   It's preferred that you use self[]= instead of this; only use this
  # when you need to set options.
  def setvar(name, value, options = EMPTY_HASH)
    if name =~ /^[0-9]+$/
      raise Puppet::ParseError, _("Cannot assign to a numeric match result variable '$%{name}'") % { name: name } # unless options[:ephemeral]
    end
    unless name.is_a? String
      raise Puppet::ParseError, _("Scope variable name %{name} is a %{class_type}, not a string") % { name: name.inspect, class_type: name.class }
    end

    # Check for reserved variable names
    if (name == VARNAME_TRUSTED || name == VARNAME_FACTS) && !options[:privileged]
      raise Puppet::ParseError, _("Attempt to assign to a reserved variable name: '%{name}'") % { name: name }
    end

    # Check for server_facts reserved variable name
    if name == VARNAME_SERVER_FACTS && !options[:privileged]
      raise Puppet::ParseError, _("Attempt to assign to a reserved variable name: '%{name}'") % { name: name }
    end

    table = effective_symtable(options[:ephemeral])
    if table.bound?(name)
      error = Puppet::ParseError.new(_("Cannot reassign variable '$%{name}'") % { name: name })
      error.file = options[:file] if options[:file]
      error.line = options[:line] if options[:line]
      raise error
    end

    table[name] = value

    # Assign the qualified name in the environment
    # Note that Settings scope has a source set to Boolean true.
    #
    # Only meaningful to set a fqn globally if table to assign to is the top of the scope's ephemeral stack
    if @symtable.equal?(table)
      if is_topscope?
        # the scope name is '::'
        compiler.qualified_variables[name] = value
      elsif source.is_a?(Puppet::Resource::Type) && source.type == :hostclass
        # the name is the name of the class
        sourcename = source.name
        compiler.qualified_variables["#{sourcename}::#{name}"] = value
      end
    end
    value
  end

  def set_trusted(hash)
    setvar('trusted', deep_freeze(hash), :privileged => true)
  end

  def set_facts(hash)
    setvar('facts', deep_freeze(hash), :privileged => true)
  end

  def set_server_facts(hash)
    setvar('server_facts', deep_freeze(hash), :privileged => true)
  end

  # Deeply freezes the given object. The object and its content must be of the types:
  # Array, Hash, Numeric, Boolean, Regexp, NilClass, or String. All other types raises an Error.
  # (i.e. if they are assignable to Puppet::Pops::Types::Data type).
  #
  def deep_freeze(object)
    case object
    when Array
      object.each { |v| deep_freeze(v) }
      object.freeze
    when Hash
      object.each { |k, v| deep_freeze(k); deep_freeze(v) }
      object.freeze
    when NilClass, Numeric, TrueClass, FalseClass
      # do nothing
    when String
      object.freeze
    else
      raise Puppet::Error, _("Unsupported data type: '%{klass}'") % { klass: object.class }
    end
    object
  end
  private :deep_freeze

  # Return the effective "table" for setting variables.
  # This method returns the first ephemeral "table" that acts as a local scope, or this
  # scope's symtable. If the parameter `use_ephemeral` is true, the "top most" ephemeral "table"
  # will be returned (irrespective of it being a match scope or a local scope).
  #
  # @param use_ephemeral [Boolean] whether the top most ephemeral (of any kind) should be used or not
  def effective_symtable(use_ephemeral)
    s = @ephemeral[-1]
    return s || @symtable if use_ephemeral

    while s && !s.is_local_scope?()
      s = s.parent
    end
    s || @symtable
  end

  # Sets the variable value of the name given as an argument to the given value. The value is
  # set in the current scope and may shadow a variable with the same name in a visible outer scope.
  # It is illegal to re-assign a variable in the same scope. It is illegal to set a variable in some other
  # scope/namespace than the scope passed to a method.
  #
  # @param varname [String] The variable name to which the value is assigned. Must not contain `::`
  # @param value [String] The value to assign to the given variable name.
  # @param options [Hash] Additional options, not part of api and no longer used.
  #
  # @api public
  #
  def []=(varname, value, _ = nil)
    setvar(varname, value)
  end

  # Used mainly for logging
  def to_s
    # As this is used for logging, this should really not be done in this class at all...
    return "Scope(#{@resource})" unless @resource.nil?

    # For logging of function-scope - it is now showing the file and line.
    detail = Puppet::Pops::PuppetStack.top_of_stack
    return "Scope()" if detail.empty?

    # shorten the path if possible
    path = detail[0]
    env_path = nil
    env_path = environment.configuration.path_to_env unless environment.nil? || environment.configuration.nil?
    # check module paths first since they may be in the environment (i.e. they are longer)
    module_path = environment.full_modulepath.detect { |m_path| path.start_with?(m_path) }
    if module_path
      path = "<module>" + path[module_path.length..]
    elsif env_path && path && path.start_with?(env_path)
      path = "<env>" + path[env_path.length..]
    end
    # Make the output appear as "Scope(path, line)"
    "Scope(#{[path, detail[1]].join(', ')})"
  end

  alias_method :inspect, :to_s

  # Pop ephemeral scopes up to level and return them
  #
  # @param level [Integer] a positive integer
  # @return [Array] the removed ephemeral scopes
  # @api private
  def pop_ephemerals(level)
    @ephemeral.pop(@ephemeral.size - level)
  end

  # Push ephemeral scopes onto the ephemeral scope stack
  # @param ephemeral_scopes [Array]
  # @api private
  def push_ephemerals(ephemeral_scopes)
    ephemeral_scopes.each { |ephemeral_scope| @ephemeral.push(ephemeral_scope) } unless ephemeral_scopes.nil?
  end

  def ephemeral_level
    @ephemeral.size
  end

  # TODO: Who calls this?
  def new_ephemeral(local_scope = false)
    if local_scope
      @ephemeral.push(LocalScope.new(@ephemeral.last))
    else
      @ephemeral.push(MatchScope.new(@ephemeral.last, nil))
    end
  end

  # Execute given block in global scope with no ephemerals present
  #
  # @yieldparam [Scope] global_scope the global and ephemeral less scope
  # @return [Object] the return of the block
  #
  # @api private
  def with_global_scope(&block)
    find_global_scope.without_ephemeral_scopes(&block)
  end

  # Execute given block with a ephemeral scope containing the given variables
  # @api private
  def with_local_scope(scope_variables)
    local = LocalScope.new(@ephemeral.last)
    scope_variables.each_pair { |k, v| local[k] = v }
    @ephemeral.push(local)
    begin
      yield(self)
    ensure
      @ephemeral.pop
    end
  end

  # Execute given block with all ephemeral popped from the ephemeral stack
  #
  # @api private
  def without_ephemeral_scopes
    save_ephemeral = @ephemeral
    begin
      @ephemeral = [@symtable]
      yield(self)
    ensure
      @ephemeral = save_ephemeral
    end
  end

  # Nests a parameter scope
  # @param [String] callee_name the name of the function, template, or resource that defines the parameters
  # @param [Array<String>] param_names list of parameter names
  # @yieldparam [ParameterScope] param_scope the nested scope
  # @api private
  def with_parameter_scope(callee_name, param_names)
    param_scope = ParameterScope.new(@ephemeral.last, callee_name, param_names)
    with_guarded_scope do
      @ephemeral.push(param_scope)
      yield(param_scope)
    end
  end

  # Execute given block and ensure that ephemeral level is restored
  #
  # @return [Object] the return of the block
  #
  # @api private
  def with_guarded_scope
    elevel = ephemeral_level
    begin
      yield
    ensure
      pop_ephemerals(elevel)
    end
  end

  # Sets match data in the most nested scope (which always is a MatchScope), it clobbers match data already set there
  #
  def set_match_data(match_data)
    @ephemeral.last.match_data = match_data
  end

  # Nests a match data scope
  def new_match_scope(match_data)
    @ephemeral.push(MatchScope.new(@ephemeral.last, match_data))
  end

  def ephemeral_from(match, file = nil, line = nil)
    case match
    when Hash
      # Create local scope ephemeral and set all values from hash
      new_ephemeral(true)
      match.each { |k, v| setvar(k, v, :file => file, :line => line, :ephemeral => true) }
      # Must always have an inner match data scope (that starts out as transparent)
      # In 3x slightly wasteful, since a new nested scope is created for a match
      # (TODO: Fix that problem)
      new_ephemeral(false)
    else
      raise(ArgumentError, _("Invalid regex match data. Got a %{klass}") % { klass: match.class }) unless match.is_a?(MatchData)

      # Create a match ephemeral and set values from match data
      new_match_scope(match)
    end
  end

  # @api private
  def find_resource_type(type)
    raise Puppet::DevError, _("Scope#find_resource_type() is no longer supported, use Puppet::Pops::Evaluator::Runtime3ResourceSupport instead")
  end

  # @api private
  def find_builtin_resource_type(type)
    raise Puppet::DevError, _("Scope#find_builtin_resource_type() is no longer supported, use Puppet::Pops::Evaluator::Runtime3ResourceSupport instead")
  end

  # @api private
  def find_defined_resource_type(type)
    raise Puppet::DevError, _("Scope#find_defined_resource_type() is no longer supported, use Puppet::Pops::Evaluator::Runtime3ResourceSupport instead")
  end

  def method_missing(method, *args, &block)
    method.to_s =~ /^function_(.*)$/
    name = ::Regexp.last_match(1)
    super unless name
    super unless Puppet::Parser::Functions.function(name)
    # In odd circumstances, this might not end up defined by the previous
    # method, so we might as well be certain.
    if respond_to? method
      send(method, *args)
    else
      raise Puppet::DevError, _("Function %{name} not defined despite being loaded!") % { name: name }
    end
  end

  # To be removed when enough time has passed after puppet 5.0.0
  # @api private
  def resolve_type_and_titles(type, titles)
    raise Puppet::DevError, _("Scope#resolve_type_and_title() is no longer supported, use Puppet::Pops::Evaluator::Runtime3ResourceSupport instead")
  end

  # Transforms references to classes to the form suitable for
  # lookup in the compiler.
  #
  # Makes names passed in the names array absolute if they are relative.
  #
  # Transforms Class[] and Resource[] type references to class name
  # or raises an error if a Class[] is unspecific, if a Resource is not
  # a 'class' resource, or if unspecific (no title).
  #
  #
  # @param names [Array<String>] names to (optionally) make absolute
  # @return [Array<String>] names after transformation
  #
  def transform_and_assert_classnames(names)
    names.map do |name|
      case name
      when NilClass
        raise ArgumentError, _("Cannot use undef as a class name")
      when String
        raise ArgumentError, _("Cannot use empty string as a class name") if name.empty?

        name.sub(/^([^:]{1,2})/, '::\1')

      when Puppet::Resource
        assert_class_and_title(name.type, name.title)
        name.title.sub(/^([^:]{1,2})/, '::\1')

      when Puppet::Pops::Types::PClassType
        # TRANSLATORS "Class" and "Type" are Puppet keywords and should not be translated
        raise ArgumentError, _("Cannot use an unspecific Class[] Type") unless name.class_name

        name.class_name.sub(/^([^:]{1,2})/, '::\1')

      when Puppet::Pops::Types::PResourceType
        assert_class_and_title(name.type_name, name.title)
        name.title.sub(/^([^:]{1,2})/, '::\1')
      end.downcase
    end
  end

  # Calls a 3.x or 4.x function by name with arguments given in an array using the 4.x calling convention
  # and returns the result.
  # Note that it is the caller's responsibility to rescue the given ArgumentError and provide location information
  # to aid the user find the problem. The problem is otherwise reported against the source location that
  # invoked the function that ultimately called this method.
  #
  # @return [Object] the result of the called function
  # @raise ArgumentError if the function does not exist
  def call_function(func_name, args, &block)
    Puppet::Pops::Parser::EvaluatingParser.new.evaluator.external_call_function(func_name, args, self, &block)
  end

  private

  def assert_class_and_title(type_name, title)
    if type_name.nil? || type_name == ''
      # TRANSLATORS "Resource" is a class name and should not be translated
      raise ArgumentError, _("Cannot use an unspecific Resource[] where a Resource['class', name] is expected")
    end
    unless type_name =~ /^[Cc]lass$/
      # TRANSLATORS "Resource" is a class name and should not be translated
      raise ArgumentError, _("Cannot use a Resource[%{type_name}] where a Resource['class', name] is expected") % { type_name: type_name }
    end
    if title.nil?
      # TRANSLATORS "Resource" is a class name and should not be translated
      raise ArgumentError, _("Cannot use an unspecific Resource['class'] where a Resource['class', name] is expected")
    end
  end

  def extend_with_functions_module
    root = Puppet.lookup(:root_environment)
    extend Puppet::Parser::Functions.environment_module(root)
    extend Puppet::Parser::Functions.environment_module(environment) if environment != root
  end
end
