# frozen_string_literal: true

module RuboCop
  module Cop
    module Lint
      # This cop checks for the presence of precise comparison of floating point numbers.
      #
      # Floating point values are inherently inaccurate, and comparing them for exact equality
      # is almost never the desired semantics. Comparison via the `==/!=` operators checks
      # floating-point value representation to be exactly the same, which is very unlikely
      # if you perform any arithmetic operations involving precision loss.
      #
      # @example
      #   # bad
      #   x == 0.1
      #   x != 0.1
      #
      #   # good - using BigDecimal
      #   x.to_d == 0.1.to_d
      #
      #   # good
      #   (x - 0.1).abs < Float::EPSILON
      #
      #   # good
      #   tolerance = 0.0001
      #   (x - 0.1).abs < tolerance
      #
      #   # Or some other epsilon based type of comparison:
      #   # https://www.embeddeduse.com/2019/08/26/qt-compare-two-floats/
      #
      class FloatComparison < Base
        MSG = 'Avoid (in)equality comparisons of floats as they are unreliable.'

        EQUALITY_METHODS = %i[== != eql? equal?].freeze
        FLOAT_RETURNING_METHODS = %i[to_f Float fdiv].freeze
        FLOAT_INSTANCE_METHODS = %i[@- abs magnitude modulo next_float prev_float quo].to_set.freeze

        RESTRICT_ON_SEND = EQUALITY_METHODS

        def on_send(node)
          lhs, _method, rhs = *node
          add_offense(node) if float?(lhs) || float?(rhs)
        end

        private

        def float?(node)
          return false unless node

          case node.type
          when :float
            true
          when :send
            check_send(node)
          when :begin
            float?(node.children.first)
          else
            false
          end
        end

        # rubocop:disable Metrics/PerceivedComplexity
        def check_send(node)
          if node.arithmetic_operation?
            lhs, _operation, rhs = *node
            float?(lhs) || float?(rhs)
          elsif FLOAT_RETURNING_METHODS.include?(node.method_name)
            true
          elsif node.receiver&.float_type?
            if FLOAT_INSTANCE_METHODS.include?(node.method_name)
              true
            else
              check_numeric_returning_method(node)
            end
          end
        end
        # rubocop:enable Metrics/PerceivedComplexity

        def check_numeric_returning_method(node)
          return false unless node.receiver

          case node.method_name
          when :angle, :arg, :phase
            Float(node.receiver.source).negative?
          when :ceil, :floor, :round, :truncate
            precision = node.first_argument
            precision&.int_type? && Integer(precision.source).positive?
          end
        end
      end
    end
  end
end
