# frozen_string_literal: true

require_relative '../../puppet/parameter/value'

# A collection of values and regular expressions, used for specifying allowed values
# in a given parameter.
# @note This class is considered part of the internal implementation of {Puppet::Parameter}, and
#   {Puppet::Property} and the functionality provided by this class should be used via their interfaces.
# @comment This class probably have several problems when trying to use it with a combination of
#   regular expressions and aliases as it finds an acceptable value holder vi "name" which may be
#   a regular expression...
#
# @api private
#
class Puppet::Parameter::ValueCollection
  # Aliases the given existing _other_ value with the additional given _name_.
  # @return [void]
  # @api private
  #
  def aliasvalue(name, other)
    other = other.to_sym
    value = match?(other)
    raise Puppet::DevError, _("Cannot alias nonexistent value %{value}") % { value: other } unless value

    value.alias(name)
  end

  # Returns a doc string (enumerating the acceptable values) for all of the values in this parameter/property.
  # @return [String] a documentation string.
  # @api private
  #
  def doc
    unless defined?(@doc)
      @doc = ''.dup
      unless values.empty?
        @doc << "Valid values are "
        @doc << @strings.collect do |value|
          aliases = value.aliases
          if aliases && !aliases.empty?
            "`#{value.name}` (also called `#{aliases.join(', ')}`)"
          else
            "`#{value.name}`"
          end
        end.join(", ") << ". "
      end

      unless regexes.empty?
        @doc << "Values can match `#{regexes.join('`, `')}`."
      end
    end

    @doc
  end

  # @return [Boolean] Returns whether the set of allowed values is empty or not.
  # @api private
  #
  def empty?
    @values.empty?
  end

  # @api private
  #
  def initialize
    # We often look values up by name, so a hash makes more sense.
    @values = {}

    # However, we want to retain the ability to match values in order,
    # but we always prefer directly equality (i.e., strings) over regex matches.
    @regexes = []
    @strings = []
  end

  # Checks if the given value is acceptable (matches one of the literal values or patterns) and returns
  # the "matcher" that matched.
  # Literal string matchers are tested first, if both a literal and a regexp match would match, the literal
  # match wins.
  #
  # @param test_value [Object] the value to test if it complies with the configured rules
  # @return [Puppet::Parameter::Value, nil] The instance of Puppet::Parameter::Value that matched the given value, or nil if there was no match.
  # @api private
  #
  def match?(test_value)
    # First look for normal values
    value = @strings.find { |v| v.match?(test_value) }
    return value if value

    # Then look for a regex match
    @regexes.find { |v| v.match?(test_value) }
  end

  # Munges the value if it is valid, else produces the same value.
  # @param value [Object] the value to munge
  # @return [Object] the munged value, or the given value
  # @todo This method does not seem to do any munging. It just returns the value if it matches the
  #   regexp, or the (most likely Symbolic) allowed value if it matches (which is more of a replacement
  #   of one instance with an equal one. Is the intent that this method should be specialized?
  # @api private
  #
  def munge(value)
    return value if empty?

    instance = match?(value)
    if instance
      if instance.regex?
        value
      else
        instance.name
      end
    else
      value
    end
  end

  # Defines a new valid value for a {Puppet::Property}.
  # A valid value is specified as a literal (typically a Symbol), but can also be
  # specified with a regexp.
  #
  # @param name [Symbol, Regexp] a valid literal value, or a regexp that matches a value
  # @param options [Hash] a hash with options
  # @option options [Symbol] :event The event that should be emitted when this value is set.
  # @todo Option :event original comment says "event should be returned...", is "returned" the correct word
  #   to use?
  # @option options [Symbol] :invalidate_refreshes True if a change on this property should invalidate and
  #   remove any scheduled refreshes (from notify or subscribe) targeted at the same resource. For example, if
  #   a change in this property takes into account any changes that a scheduled refresh would have performed,
  #   then the scheduled refresh would be deleted.
  # @option options [Object] _any_ Any other option is treated as a call to a setter having the given
  #   option name (e.g. `:required_features` calls `required_features=` with the option's value as an
  #   argument).
  # @api private
  #
  def newvalue(name, options = {}, &block)
    call_opt = options[:call]
    unless call_opt.nil?
      devfail "Cannot use obsolete :call value '#{call_opt}' for property '#{self.class.name}'" unless call_opt == :none || call_opt == :instead
      # TRANSLATORS ':call' is a property and should not be translated
      message = _("Property option :call is deprecated and no longer used.")
      message += ' ' + _("Please remove it.")
      Puppet.deprecation_warning(message)
      options = options.reject { |k, _v| k == :call }
    end

    value = Puppet::Parameter::Value.new(name)
    @values[value.name] = value
    if value.regex?
      @regexes << value
    else
      @strings << value
    end

    options.each { |opt, arg| value.send(opt.to_s + "=", arg) }
    if block_given?
      devfail "Cannot use :call value ':none' in combination with a block for property '#{self.class.name}'" if call_opt == :none
      value.block = block
      value.method ||= "set_#{value.name}" unless value.regex?
    elsif call_opt == :instead
      devfail "Cannot use :call value ':instead' without a block for property '#{self.class.name}'"
    end
    value
  end

  # Defines one or more valid values (literal or regexp) for a parameter or property.
  # @return [void]
  # @dsl type
  # @api private
  #
  def newvalues(*names)
    names.each { |name| newvalue(name) }
  end

  # @return [Array<String>] An array of the regular expressions in string form, configured as matching valid values.
  # @api private
  #
  def regexes
    @regexes.collect { |r| r.name.inspect }
  end

  # Validates the given value against the set of valid literal values and regular expressions.
  # @raise [ArgumentError] if the value is not accepted
  # @return [void]
  # @api private
  #
  def validate(value)
    return if empty?

    unless @values.detect { |_name, v| v.match?(value) }
      str = _("Invalid value %{value}.") % { value: value.inspect }
      str += " " + _("Valid values are %{value_list}.") % { value_list: values.join(", ") } unless values.empty?
      str += " " + _("Valid values match %{pattern}.") % { pattern: regexes.join(", ") } unless regexes.empty?
      raise ArgumentError, str
    end
  end

  # Returns a valid value matcher (a literal or regular expression)
  # @todo This looks odd, asking for an instance that matches a symbol, or an instance that has
  #   a regexp. What is the intention here? Marking as api private...
  #
  # @return [Puppet::Parameter::Value] a valid value matcher
  # @api private
  #
  def value(name)
    @values[name]
  end

  # @return [Array<Symbol>] Returns a list of valid literal values.
  # @see regexes
  # @api private
  #
  def values
    @strings.collect(&:name)
  end
end
