# Copyright (c) 2022 Contrast Security, Inc. See https://www.contrastsecurity.com/enduser-terms-0317a for more details.
# frozen_string_literal: true

module Contrast
  module Utils
    # Utility methods for identifying instances that can be used interchangeably
    module DuckUtils
      class << self
        # Determine if the given object, or the object to which it delegates,
        # responds to the given method.
        #
        # @param object [Object]
        # @param method [Symbol]
        # @return [Boolean]
        def quacks_to? object, method
          object.cs__respond_to?(method)
          return true if object.cs__respond_to?(method)
          return false unless object.is_a?(Delegator)

          object.cs__delegator_respond_to?(method)
        end

        # Most things that are closable IO's will in fact be of the IO type. That
        # being said, some will also be extensions of DelegateClass with IO type,
        # like Tempfile. We need to handle both cases.
        #
        # @param object [Object]
        # @return [Boolean]
        def closable_io? object
          return false unless Contrast::Utils::IOUtil.io?(object)

          quacks_to?(object, :closed?)
        end

        # Determine if the given Object is a Hash, or similar enough to a hash
        # for us to iterate on it using the #each_pair method
        #
        # @param object [Object]
        # @return [Boolean]
        def iterable_hash? object
          # do iterate on things believed to safely implement #each_pair
          return true if object.cs__is_a?(Hash)

          # otherwise, don't risk it
          false
        end

        # Determine if the given Object is a concrete implementation of
        # Enumerable known to be safe to call #each on.
        #
        # @param object [Object]
        # @return [Boolean]
        def iterable_enumerable? object
          # do iterate on things believed to safely implement #each. We're
          # purposefully skipping Hash and Hash-like things here as
          # #iterable_hash? should handle those.
          return true if object.cs__is_a?(Array)
          return true if object.cs__is_a?(Enumerator)
          return true if object.cs__is_a?(Hash)
          return true if object.cs__is_a?(Range)
          return true if object.cs__is_a?(Set)

          # otherwise, don't risk it
          false
        end

        # Every duck quacks to nil, we need to check if it is empty?
        # Utils method to check for blank ( nil or empty object ).
        #
        # @param [Object] object to test.
        # @return [Boolean]
        def empty_duck? object
          # If not quacking to empty it is True/False class, Integer, Regexp or Time instance.
          return false if object.instance_of?(TrueClass) || object.instance_of?(FalseClass)

          if object.cs__respond_to?(:empty?)
            return true if object.empty?
          elsif object.nil?
            return true
          end

          false
        end
      end
    end
  end
end