# frozen_string_literal: true

module Interfaceable
  # Checks if class implements interfaces correctly
  class ImplementationCheck
    def initialize(klass)
      @klass = klass
    end

    def perform(interfaces)
      results = validate(interfaces)

      results.reject do |_, checks|
        checks.values.all?(&:empty?)
      end
    end

    private

    # rubocop:disable Metrics/MethodLength
    def validate(interfaces)
      interfaces.each_with_object({}) do |interface, acc|
        missing_class_methods = find_missing_class_methods(interface)
        missing_instance_methods = find_missing_instance_methods(interface)
        instance_method_signature_errors = find_signature_errors(
          interface,
          :instance_method,
          interface.instance_methods - missing_instance_methods
        )
        class_method_signature_errors = find_signature_errors(
          interface,
          :method,
          own_methods(interface.methods) - missing_class_methods
        )

        acc[interface] = {
          missing_instance_methods: missing_instance_methods,
          missing_class_methods: missing_class_methods,
          instance_method_signature_errors: instance_method_signature_errors,
          class_method_signature_errors: class_method_signature_errors
        }
      end
    end
    # rubocop:enable Metrics/MethodLength

    def find_missing_class_methods(interface)
      own_methods(interface.methods).reject do |meth|
        own_methods(@klass.methods).include?(meth)
      end
    end

    def find_missing_instance_methods(interface)
      interface.instance_methods.reject do |meth|
        @klass.instance_methods.include?(meth)
      end
    end

    def find_signature_errors(interface, method_type, implemented_methods)
      implemented_methods.each_with_object({}) do |meth, acc|
        expected_parameters = interface.send(method_type, meth).parameters
        actual_parameters = @klass.send(method_type, meth).parameters

        next unless (errors = check_method_signature(expected_parameters, actual_parameters))

        acc[meth] = {
          expected: errors[:expected_positional_parameters] + errors[:expected_keyword_parameters],
          actual: errors[:actual_positional_parameters] + errors[:actual_keyword_parameters]
        }
      end
    end

    def own_methods(methods)
      methods - Object.methods
    end

    OPTIONAL_PARAMETERS = %w[opt rest keyrest]

    def check_if_parameters_are_compatible(expected_parameters, actual_parameters)
      return false if actual_parameters.length < expected_parameters.length
      return false if actual_parameters.take(expected_parameters.length) != expected_parameters

      additional_parameters = actual_parameters[expected_parameters.length..]
      additional_parameters.all? { OPTIONAL_PARAMETERS.include?(_1) }
    end

    # rubocop:disable Metrics/MethodLength
    def check_method_signature(expected_parameters, actual_parameters)
      expected_keyword_parameters, expected_positional_parameters = simplify_parameters(
        *split_parameters_by_type(expected_parameters)
      )
      actual_keyword_parameters, actual_positional_parameters = simplify_parameters(
        *split_parameters_by_type(actual_parameters)
      )

      return if expected_positional_parameters == actual_positional_parameters &&
                expected_keyword_parameters == actual_keyword_parameters

      return if check_if_parameters_are_compatible(expected_positional_parameters, actual_positional_parameters) &&
                check_if_parameters_are_compatible(expected_keyword_parameters, actual_keyword_parameters)

      {
        expected_positional_parameters: expected_positional_parameters,
        expected_keyword_parameters: expected_keyword_parameters,
        actual_positional_parameters: actual_positional_parameters,
        actual_keyword_parameters: actual_keyword_parameters
      }
    end
    # rubocop:enable Metrics/MethodLength

    def simplify_parameters(keyword_parameters, positional_parameters)
      keyrest = pop_keyrest(keyword_parameters)

      [
        keyword_parameters.map(&:last).sort + (keyrest ? ['keyrest'] : []),
        positional_parameters.map(&:first).reject { |p| p == :block }.map(&:to_s)
      ]
    end

    def pop_keyrest(keyword_parameters)
      return unless keyword_parameters.last && keyword_parameters.last.first == :keyrest

      keyword_parameters.pop
    end

    def split_parameters_by_type(parameters)
      parameters.partition { |(arg_type)| arg_type.to_s =~ /^key/ }
    end
  end
end
