module Karafka
  module Helpers
    # Class used to autodetect corresponding classes that are internally inside Karafka framework
    # It is used among others to match:
    #   controller => worker
    #   controller => responder
    class ClassMatcher
      # Regexp used to remove any non classy like characters that might be in the controller
      # class name (if defined dynamically, etc)
      CONSTANT_REGEXP = %r{[?!=+\-\*/\^\|&\[\]<>%~\#\:\s\(\)]}

      # @param klass [Class] class to which we want to find a corresponding class
      # @param from [String] what type of object is it (based on postfix name part)
      # @param to [String] what are we looking for (based on a postfix name part)
      # @example Controller that has a corresponding worker
      #   matcher = Karafka::Helpers::ClassMatcher.new(SuperController, 'Controller', 'Worker')
      #   matcher.match #=> SuperWorker
      # @example Controller without a corresponding worker
      #   matcher = Karafka::Helpers::ClassMatcher.new(Super2Controller, 'Controller', 'Worker')
      #   matcher.match #=> nil
      def initialize(klass, from:, to:)
        @klass = klass
        @from = from
        @to = to
      end

      # @return [Class] matched class
      # @return [nil] nil if we couldn't find matching class
      def match
        return nil if name.empty?
        return nil unless scope.const_defined?(name)
        matching = scope.const_get(name)
        same_scope?(matching) ? matching : nil
      end

      # @return [String] name of a new class that we're looking for
      # @note This method returns name of a class without a namespace
      # @example From SuperController matching worker
      #   matcher.name #=> 'SuperWorker'
      # @example From Namespaced::Super2Controller matching worker
      #   matcher.name #=> Super2Worker
      def name
        inflected = @klass.to_s.split('::').last.to_s
        inflected.gsub!(@from, @to)
        inflected.gsub!(CONSTANT_REGEXP, '')
        inflected
      end

      # @return [Class, Module] class or module in which we're looking for a matching
      def scope
        scope_of(@klass)
      end

      private

      # @param klass [Class] class for which we want to extract it's enclosing class/module
      # @return [Class, Module] enclosing class/module
      # @return [::Object] object if it was a root class
      #
      # @example Non-namespaced class
      #   scope_of(SuperClass) #=> Object
      # @example Namespaced class
      #   scope_of(Abc::SuperClass) #=> Abc
      def scope_of(klass)
        enclosing = klass.to_s.split('::')[0...-1]
        return ::Object if enclosing.empty?
        ::Object.const_get(enclosing.join('::'))
      end

      # @param matching [Class] class of which scope we want to check
      # @return [Boolean] true if the scope of class is the same as scope of matching
      def same_scope?(matching)
        scope == scope_of(matching)
      end
    end
  end
end