# frozen_string_literal: true

module Algebra
  class Monomial
    module Mixins
      # Module to mix in {Algebra::Monomial}. Provides support of standard
      # `Comparable` module.
      module Comparable
        include ::Comparable

        # Accepts a numeric or a monomial as its argument, and returns result
        # of comparison with the argument. If argument is neither monomial nor
        # numeric, then returns `nil`.
        # @see compare_with_numeric
        # @see compare_with_monomial
        # @param [Numeric, Algebra::Monomial] other
        #   object to compare with
        # @return [Integer]
        #   result of comparison
        def <=>(other)
          case other
          when Numeric
            compare_with_numeric(other)
          when Algebra::Monomial
            compare_with_monomial(other)
          end
        end

        private

        # Returns result of comparison with provided numeric by the following
        # rules:
        # *   any monomial with variables is always more than any numeric;
        # *   monomial without variables reduces comparison to comparison with
        #     its coefficient
        # @example
        #   12xy^2 <=> 1 ~> 1
        # @example
        #   1 <=> 2 ~> -1
        # @param [Numeric] other
        #   provided numeric
        # @return [Integer]
        #   result of the comparison
        def compare_with_numeric(other)
          return 1 unless numeric?

          coef <=> other
        end

        # Sequentially tries to reduce comparison with provided monomial to
        # a number of rules and returns result of reducing
        # @see compare_numerically
        # @see compare_degrees
        # @see compare_sizes
        # @see compare_degs
        # @example
        #   12xy^2 <=> 1 ~> 1
        # @example
        #   2 <=> x^2 ~> -1
        # @example
        #   2 <=> 1 ~> 1
        # @param [Algebra::Monomial] other
        #   provided monomial
        # @return [Integer]
        #   result of comparison
        def compare_with_monomial(other)
          compare_numerically(other) ||
            compare_degrees(other) ||
            compare_sizes(other) ||
            compare_degs(other)
        end

        # Tries to reduce comparison with provided monomial to comparison with
        # numeric, using the same rules as {compare_with_numeric}, and returns
        # result of the comparison in case of successful reducing, or `nil`
        # otherwise
        # @example
        #   12xy^2 <=> 1 ~> 1
        # @example
        #   2 <=> x^2 ~> -1
        # @example
        #   2 <=> 1 ~> 1
        # @example
        #   12xy^2 <=> x^2 ~> 1
        # @example
        #   x <=> t^5s^6 ~> -1
        # @example
        #   x^6 <=> xy^2z^3 ~> 1
        # @example
        #   xy^2z^3 <=> xy^7 ~> -1
        # @example
        #   xy^2z^3t^4 <=> xy^2z^3s^4 ~> 1
        # @example
        #   xy^2z^4t^3 <=> xy^2z^3t^4 ~> 1
        # @example
        #   xy^2z^4t^3 <=> xy^2z^4t^3 ~> 0
        # @param [Algebra::Monomial] other
        #   provided monomial
        # @return [Integer]
        #   result of comparison
        # @return [NilClass]
        #   if reducing to rules of {compare_with_numeric} wasn't successful
        def compare_numerically(other)
          if other.numeric?
            compare_with_numeric(other.coef)
          elsif numeric?
            -1
          end
        end

        # Tries to reduce comparison with provided monomial to comparison of
        # their degrees, supposing that both monomials have variables (or
        # otherwise there can appear nasty uncomparable `nil` in case of zero
        # monomial), and returns result of the comparison, if the degrees are
        # not equal, or `nil` otherwise
        # @example
        #   12xy^2 <=> x^2 ~> 3 <=> 2 ~> 1
        # @example
        #   x <=> t^5s^6 ~> 1 <=> 11 ~> -1
        # @example
        #   x^6 <=> xy^2z^3 ~> 6 <=> 6 ~> nil
        # @param [Algebra::Monomial] other
        #   provided monomial
        # @return [Integer]
        #   result of comparison
        # @return [NilClass]
        #   if the degrees are equal
        def compare_degrees(other)
          c = degree <=> other.degree
          c unless c.zero?
        end

        # Tries to reduce comparison with provided monomial to comparison of
        # amounts of their variables, and returns inverse result of the
        # comparison, if the amounts are not equal, or `nil` otherwise
        # @example
        #   x^6 <=> xy^2z^3 ~> 1 <=> 3 ~> 1 (inverse)
        # @example
        #   xy^2z^3 <=> xy^7 ~> 3 <=> 2 ~> -1 (inverse)
        # @example
        #   xy^2z^3t^4 <=> xy^2z^3s^4 ~> 4 <=> 4 ~> nil
        # @example
        #   xy^2z^4t^3 <=> xy^2z^3t^4 ~> 4 <=> 4 ~> nil
        def compare_sizes(other)
          c = other.size <=> size
          c unless c.zero?
        end

        # Checks its variables and degrees with variables and degrees of
        # provided monomial, supposing that they both have the same size,
        # looking for an index, at which variables of the monomial are
        # different, or variables are the same but their degrees are different.
        # If there is such an index, returns inverse result of comparison in
        # the first case, and direct result of comparison in the second. If
        # there is no such an index, returns result of comparsion of
        # coefficients.
        # @example
        #   xy^2z^3t^4 <=> xy^2z^3s^4 ~> t <=> s ~> 1 (inverse)
        # @example
        #   xy^2z^4t^3 <=> xy^2z^3t^4 ~> 4 <=> 3 ~> 1 (direct)
        # @example
        #   2xy^2z^4t^3 <=> 3xy^2z^4t^3 ~> 2 <=> 3 ~> -1 (no index found)
        def compare_degs(other)
          result = each_with_index do |(name, deg), i|
            c = other.names[i] <=> name
            break c unless c.zero?

            c = deg <=> other.degs[i]
            break c unless c.zero?
          end
          result == self ? coef <=> other.coef : result
        end
      end
    end
  end
end
