# frozen_string_literal: true

# Provides feature definitions.
require_relative '../../puppet/util/docs'
require_relative '../../puppet/util'
# This module models provider features and handles checking whether the features
# are present.
# @todo Unclear what is api and what is private in this module.
#
module Puppet::Util::ProviderFeatures
  include Puppet::Util::Docs
  # This class models provider features and handles checking whether the features
  # are present.
  # @todo Unclear what is api and what is private in this class
  class ProviderFeature
    include Puppet::Util
    include Puppet::Util::Docs
    attr_accessor :name, :docs, :methods

    # Are all of the requirements met?
    # Requirements are checked by checking if feature predicate methods have been generated - see {#methods_available?}.
    # @param obj [Object, Class] the object or class to check if requirements are met
    # @return [Boolean] whether all requirements for this feature are met or not.
    def available?(obj)
      if methods
        !!methods_available?(obj)
      else
        # In this case, the provider has to declare support for this
        # feature, and that's been checked before we ever get to the
        # method checks.
        false
      end
    end

    def initialize(name, docs, methods: nil)
      self.name = name.intern
      self.docs = docs
      @methods = methods
    end

    private

    # Checks whether all feature predicate methods are available.
    # @param obj [Object, Class] the object or class to check if feature predicates are available or not.
    # @return [Boolean] Returns whether all of the required methods are available or not in the given object.
    def methods_available?(obj)
      methods.each do |m|
        if obj.is_a?(Class)
          return false unless obj.public_method_defined?(m)
        else
          return false unless obj.respond_to?(m)
        end
      end
      true
    end
  end

  # Defines one feature.
  # At a minimum, a feature requires a name
  # and docs, and at this point they should also specify a list of methods
  # required to determine if the feature is present.
  # @todo How methods that determine if the feature is present are specified.
  def feature(name, docs, hash = {})
    @features ||= {}
    raise Puppet::DevError, _("Feature %{name} is already defined") % { name: name } if @features.include?(name)

    begin
      obj = ProviderFeature.new(name, docs, **hash)
      @features[obj.name] = obj
    rescue ArgumentError => detail
      error = ArgumentError.new(
        _("Could not create feature %{name}: %{detail}") % { name: name, detail: detail }
      )
      error.set_backtrace(detail.backtrace)
      raise error
    end
  end

  # @return [String] Returns a string with documentation covering all features.
  def featuredocs
    str = ''.dup
    @features ||= {}
    return nil if @features.empty?

    names = @features.keys.sort_by(&:to_s)
    names.each do |name|
      doc = @features[name].docs.gsub(/\n\s+/, " ")
      str << "- *#{name}*: #{doc}\n"
    end

    if providers.length > 0
      headers = ["Provider", names].flatten
      data = {}
      providers.each do |provname|
        data[provname] = []
        prov = provider(provname)
        names.each do |name|
          if prov.feature?(name)
            data[provname] << "*X*"
          else
            data[provname] << ""
          end
        end
      end
      str << doctable(headers, data)
    end
    str
  end

  # @return [Array<String>] Returns a list of features.
  def features
    @features ||= {}
    @features.keys
  end

  # Generates a module that sets up the boolean predicate methods to test for given features.
  #
  def feature_module
    unless defined?(@feature_module)
      @features ||= {}
      @feature_module = ::Module.new
      const_set("FeatureModule", @feature_module)
      features = @features
      # Create a feature? method that can be passed a feature name and
      # determine if the feature is present.
      @feature_module.send(:define_method, :feature?) do |name|
        method = name.to_s + "?"
        return !!(respond_to?(method) and send(method))
      end

      # Create a method that will list all functional features.
      @feature_module.send(:define_method, :features) do
        return false unless defined?(features)

        features.keys.find_all { |n| feature?(n) }.sort_by(&:to_s)
      end

      # Create a method that will determine if a provided list of
      # features are satisfied by the curred provider.
      @feature_module.send(:define_method, :satisfies?) do |*needed|
        ret = true
        needed.flatten.each do |feature|
          unless feature?(feature)
            ret = false
            break
          end
        end
        ret
      end

      # Create a boolean method for each feature so you can test them
      # individually as you might need.
      @features.each do |name, feature|
        method = name.to_s + "?"
        @feature_module.send(:define_method, method) do
          (is_a?(Class) ? declared_feature?(name) : self.class.declared_feature?(name)) or feature.available?(self)
        end
      end

      # Allow the provider to declare that it has a given feature.
      @feature_module.send(:define_method, :has_features) do |*names|
        @declared_features ||= []
        names.each do |name|
          @declared_features << name.intern
        end
      end
      # Aaah, grammatical correctness
      @feature_module.send(:alias_method, :has_feature, :has_features)
    end
    @feature_module
  end

  # @return [ProviderFeature] Returns a provider feature instance by name.
  # @param name [String] the name of the feature to return
  # @note Should only be used for testing.
  # @api private
  #
  def provider_feature(name)
    return nil unless defined?(@features)

    @features[name]
  end
end
