require 'delegate'

module Dio
  module Forwarders
    # A more controlled forwarder that targets only `attr_` type methods like
    # `attr_reader` and `attr_accessor`. These attributes are found by diffing
    # instance variables and method names to find generated accessor methods.
    #
    # It should be noted that this could be faster if there was an assumption
    # of purity at the time of the match. I may make another variant for that.
    #
    # @author [baweaver]
    # @since 0.0.3
    #
    class AttributeForwarder < BaseForwarder
      # Wrapper for creating a new Forwarder
      NEW_DIVE = -> v { new(v) }

      def initialize(base_object)
        ivars = Set.new base_object
          .instance_variables
          .map { _1.to_s.delete('@').to_sym }

        all_methods = Set.new base_object.methods

        @attributes = ivars.intersection(all_methods)

        super
      end

      # Deconstructs from a list of attribute names, wrapping each value
      # in a new dive in case the pattern match goes further than one level.
      #
      # @return [Array]
      def deconstruct
        return @base_object.map(&NEW_DIVE) if @base_object.is_a?(Array)

        @attributes.map { NEW_DIVE[@base_object.public_send(_1)] }
      end

      # Deconstructs attributes from an object, wrapping each value in a new
      # dive in case the pattern match goes further than one level.
      #
      # @param keys [Array]
      #   Keys to be extracted, diffed against possible attributes
      #
      # @raises [Dio::Errors::UnknownAttributesProvided]
      #   Guard against unknown attributes without an associated accessor
      #   method
      #
      # @return [Hash]
      def deconstruct_keys(keys)
        key_set      = Set.new(keys)
        unknown_keys = key_set - @attributes

        if unknown_keys.any?
          raise Dio::Errors::UnknownAttributesProvided.new(unknown_keys)
        end

        known_keys = @attributes.intersection(key_set)

        known_keys.to_h { [_1, NEW_DIVE[@base_object.public_send(_1)]] }
      end

      # Unwrapped context, aliased afterwards to use Ruby's delegator interface
      #
      # @return [Any]
      #   Originally wrapped object
      def value
        @base_object
      end

      alias_method :__getobj__, :value
    end
  end
end