module Sass::Script::Value
  # A SassScript object representing a number.
  # SassScript numbers can have decimal values,
  # and can also have units.
  # For example, `12`, `1px`, and `10.45em`
  # are all valid values.
  #
  # Numbers can also have more complex units, such as `1px*em/in`.
  # These cannot be inputted directly in Sass code at the moment.
  class Number < Base
    # The Ruby value of the number.
    #
    # @return [Numeric]
    attr_reader :value

    # A list of units in the numerator of the number.
    # For example, `1px*em/in*cm` would return `["px", "em"]`
    # @return [Array<String>]
    attr_reader :numerator_units

    # A list of units in the denominator of the number.
    # For example, `1px*em/in*cm` would return `["in", "cm"]`
    # @return [Array<String>]
    attr_reader :denominator_units

    # The original representation of this number.
    # For example, although the result of `1px/2px` is `0.5`,
    # the value of `#original` is `"1px/2px"`.
    #
    # This is only non-nil when the original value should be used as the CSS value,
    # as in `font: 1px/2px`.
    #
    # @return [Boolean, nil]
    attr_accessor :original

    def self.precision
      Thread.current[:sass_numeric_precision] || Thread.main[:sass_numeric_precision] || 10
    end

    # Sets the number of digits of precision
    # For example, if this is `3`,
    # `3.1415926` will be printed as `3.142`.
    # The numeric precision is stored as a thread local for thread safety reasons.
    # To set for all threads, be sure to set the precision on the main thread.
    def self.precision=(digits)
      Thread.current[:sass_numeric_precision] = digits.round
      Thread.current[:sass_numeric_precision_factor] = nil
      Thread.current[:sass_numeric_epsilon] = nil
    end

    # the precision factor used in numeric output
    # it is derived from the `precision` method.
    def self.precision_factor
      Thread.current[:sass_numeric_precision_factor] ||= 10.0**precision
    end

    # Used in checking equality of floating point numbers. Any
    # numbers within an `epsilon` of each other are considered functionally equal.
    # The value for epsilon is one tenth of the current numeric precision.
    def self.epsilon
      Thread.current[:sass_numeric_epsilon] ||= 1 / (precision_factor * 10)
    end

    # Used so we don't allocate two new arrays for each new number.
    NO_UNITS = []

    # @param value [Numeric] The value of the number
    # @param numerator_units [::String, Array<::String>] See \{#numerator\_units}
    # @param denominator_units [::String, Array<::String>] See \{#denominator\_units}
    def initialize(value, numerator_units = NO_UNITS, denominator_units = NO_UNITS)
      numerator_units = [numerator_units] if numerator_units.is_a?(::String)
      denominator_units = [denominator_units] if denominator_units.is_a?(::String)
      super(value)
      @numerator_units = numerator_units
      @denominator_units = denominator_units
      @options = nil
      normalize!
    end

    # The SassScript `+` operation.
    # Its functionality depends on the type of its argument:
    #
    # {Number}
    # : Adds the two numbers together, converting units if possible.
    #
    # {Color}
    # : Adds this number to each of the RGB color channels.
    #
    # {Value}
    # : See {Value::Base#plus}.
    #
    # @param other [Value] The right-hand side of the operator
    # @return [Value] The result of the operation
    # @raise [Sass::UnitConversionError] if `other` is a number with incompatible units
    def plus(other)
      if other.is_a? Number
        operate(other, :+)
      elsif other.is_a?(Color)
        other.plus(self)
      else
        super
      end
    end

    # The SassScript binary `-` operation (e.g. `$a - $b`).
    # Its functionality depends on the type of its argument:
    #
    # {Number}
    # : Subtracts this number from the other, converting units if possible.
    #
    # {Value}
    # : See {Value::Base#minus}.
    #
    # @param other [Value] The right-hand side of the operator
    # @return [Value] The result of the operation
    # @raise [Sass::UnitConversionError] if `other` is a number with incompatible units
    def minus(other)
      if other.is_a? Number
        operate(other, :-)
      else
        super
      end
    end

    # The SassScript unary `+` operation (e.g. `+$a`).
    #
    # @return [Number] The value of this number
    def unary_plus
      self
    end

    # The SassScript unary `-` operation (e.g. `-$a`).
    #
    # @return [Number] The negative value of this number
    def unary_minus
      Number.new(-value, @numerator_units, @denominator_units)
    end

    # The SassScript `*` operation.
    # Its functionality depends on the type of its argument:
    #
    # {Number}
    # : Multiplies the two numbers together, converting units appropriately.
    #
    # {Color}
    # : Multiplies each of the RGB color channels by this number.
    #
    # @param other [Number, Color] The right-hand side of the operator
    # @return [Number, Color] The result of the operation
    # @raise [NoMethodError] if `other` is an invalid type
    def times(other)
      if other.is_a? Number
        operate(other, :*)
      elsif other.is_a? Color
        other.times(self)
      else
        raise NoMethodError.new(nil, :times)
      end
    end

    # The SassScript `/` operation.
    # Its functionality depends on the type of its argument:
    #
    # {Number}
    # : Divides this number by the other, converting units appropriately.
    #
    # {Value}
    # : See {Value::Base#div}.
    #
    # @param other [Value] The right-hand side of the operator
    # @return [Value] The result of the operation
    def div(other)
      if other.is_a? Number
        res = operate(other, :/)
        if original && other.original
          res.original = "#{original}/#{other.original}"
        end
        res
      else
        super
      end
    end

    # The SassScript `%` operation.
    #
    # @param other [Number] The right-hand side of the operator
    # @return [Number] This number modulo the other
    # @raise [NoMethodError] if `other` is an invalid type
    # @raise [Sass::UnitConversionError] if `other` has incompatible units
    def mod(other)
      if other.is_a?(Number)
        return Number.new(Float::NAN) if other.value == 0
        operate(other, :%)
      else
        raise NoMethodError.new(nil, :mod)
      end
    end

    # The SassScript `==` operation.
    #
    # @param other [Value] The right-hand side of the operator
    # @return [Boolean] Whether this number is equal to the other object
    def eq(other)
      return Bool::FALSE unless other.is_a?(Sass::Script::Value::Number)
      this = self
      begin
        if unitless?
          this = this.coerce(other.numerator_units, other.denominator_units)
        else
          other = other.coerce(@numerator_units, @denominator_units)
        end
      rescue Sass::UnitConversionError
        return Bool::FALSE
      end
      Bool.new(basically_equal?(this.value, other.value))
    end

    def hash
      [value, numerator_units, denominator_units].hash
    end

    # Hash-equality works differently than `==` equality for numbers.
    # Hash-equality must be transitive, so it just compares the exact value,
    # numerator units, and denominator units.
    def eql?(other)
      basically_equal?(value, other.value) && numerator_units == other.numerator_units &&
        denominator_units == other.denominator_units
    end

    # The SassScript `>` operation.
    #
    # @param other [Number] The right-hand side of the operator
    # @return [Boolean] Whether this number is greater than the other
    # @raise [NoMethodError] if `other` is an invalid type
    def gt(other)
      raise NoMethodError.new(nil, :gt) unless other.is_a?(Number)
      operate(other, :>)
    end

    # The SassScript `>=` operation.
    #
    # @param other [Number] The right-hand side of the operator
    # @return [Boolean] Whether this number is greater than or equal to the other
    # @raise [NoMethodError] if `other` is an invalid type
    def gte(other)
      raise NoMethodError.new(nil, :gte) unless other.is_a?(Number)
      operate(other, :>=)
    end

    # The SassScript `<` operation.
    #
    # @param other [Number] The right-hand side of the operator
    # @return [Boolean] Whether this number is less than the other
    # @raise [NoMethodError] if `other` is an invalid type
    def lt(other)
      raise NoMethodError.new(nil, :lt) unless other.is_a?(Number)
      operate(other, :<)
    end

    # The SassScript `<=` operation.
    #
    # @param other [Number] The right-hand side of the operator
    # @return [Boolean] Whether this number is less than or equal to the other
    # @raise [NoMethodError] if `other` is an invalid type
    def lte(other)
      raise NoMethodError.new(nil, :lte) unless other.is_a?(Number)
      operate(other, :<=)
    end

    # @return [String] The CSS representation of this number
    # @raise [Sass::SyntaxError] if this number has units that can't be used in CSS
    #   (e.g. `px*in`)
    def to_s(opts = {})
      return original if original
      raise Sass::SyntaxError.new("#{inspect} isn't a valid CSS value.") unless legal_units?
      inspect
    end

    # Returns a readable representation of this number.
    #
    # This representation is valid CSS (and valid SassScript)
    # as long as there is only one unit.
    #
    # @return [String] The representation
    def inspect(opts = {})
      return original if original

      value = self.class.round(self.value)
      str = value.to_s

      # Ruby will occasionally print in scientific notation if the number is
      # small enough. That's technically valid CSS, but it's not well-supported
      # and confusing.
      str = ("%0.#{self.class.precision}f" % value).gsub(/0*$/, '') if str.include?('e')

      # Sometimes numeric formatting will result in a decimal number with a trailing zero (x.0)
      if str =~ /(.*)\.0$/
        str = $1
      end

      # We omit a leading zero before the decimal point in compressed mode.
      if @options && options[:style] == :compressed
        str.sub!(/^(-)?0\./, '\1.')
      end

      unitless? ? str : "#{str}#{unit_str}"
    end
    alias_method :to_sass, :inspect

    # @return [Integer] The integer value of the number
    # @raise [Sass::SyntaxError] if the number isn't an integer
    def to_i
      super unless int?
      value.to_i
    end

    # @return [Boolean] Whether or not this number is an integer.
    def int?
      basically_equal?(value % 1, 0.0)
    end

    # @return [Boolean] Whether or not this number has no units.
    def unitless?
      @numerator_units.empty? && @denominator_units.empty?
    end

    # Checks whether the number has the numerator unit specified.
    #
    # @example
    #   number = Sass::Script::Value::Number.new(10, "px")
    #   number.is_unit?("px") => true
    #   number.is_unit?(nil) => false
    #
    # @param unit [::String, nil] The unit the number should have or nil if the number
    #   should be unitless.
    # @see Number#unitless? The unitless? method may be more readable.
    def is_unit?(unit)
      if unit
        denominator_units.size == 0 && numerator_units.size == 1 && numerator_units.first == unit
      else
        unitless?
      end
    end

    # @return [Boolean] Whether or not this number has units that can be represented in CSS
    #   (that is, zero or one \{#numerator\_units}).
    def legal_units?
      (@numerator_units.empty? || @numerator_units.size == 1) && @denominator_units.empty?
    end

    # Returns this number converted to other units.
    # The conversion takes into account the relationship between e.g. mm and cm,
    # as well as between e.g. in and cm.
    #
    # If this number has no units, it will simply return itself
    # with the given units.
    #
    # An incompatible coercion, e.g. between px and cm, will raise an error.
    #
    # @param num_units [Array<String>] The numerator units to coerce this number into.
    #   See {\#numerator\_units}
    # @param den_units [Array<String>] The denominator units to coerce this number into.
    #   See {\#denominator\_units}
    # @return [Number] The number with the new units
    # @raise [Sass::UnitConversionError] if the given units are incompatible with the number's
    #   current units
    def coerce(num_units, den_units)
      Number.new(if unitless?
                   value
                 else
                   value * coercion_factor(@numerator_units, num_units) /
                     coercion_factor(@denominator_units, den_units)
                 end, num_units, den_units)
    end

    # @param other [Number] A number to decide if it can be compared with this number.
    # @return [Boolean] Whether or not this number can be compared with the other.
    def comparable_to?(other)
      operate(other, :+)
      true
    rescue Sass::UnitConversionError
      false
    end

    # Returns a human readable representation of the units in this number.
    # For complex units this takes the form of:
    # numerator_unit1 * numerator_unit2 / denominator_unit1 * denominator_unit2
    # @return [String] a string that represents the units in this number
    def unit_str
      rv = @numerator_units.sort.join("*")
      if @denominator_units.any?
        rv << "/"
        rv << @denominator_units.sort.join("*")
      end
      rv
    end

    private

    # @private
    # @see Sass::Script::Number.basically_equal?
    def basically_equal?(num1, num2)
      self.class.basically_equal?(num1, num2)
    end

    # Checks whether two numbers are within an epsilon of each other.
    # @return [Boolean]
    def self.basically_equal?(num1, num2)
      (num1 - num2).abs < epsilon
    end

    # @private
    def self.round(num)
      if num.is_a?(Float) && (num.infinite? || num.nan?)
        num
      elsif basically_equal?(num % 1, 0.0)
        num.round
      else
        ((num * precision_factor).round / precision_factor).to_f
      end
    end

    OPERATIONS = [:+, :-, :<=, :<, :>, :>=, :%]

    def operate(other, operation)
      this = self
      if OPERATIONS.include?(operation)
        if unitless?
          this = this.coerce(other.numerator_units, other.denominator_units)
        else
          other = other.coerce(@numerator_units, @denominator_units)
        end
      end
      # avoid integer division
      value = :/ == operation ? this.value.to_f : this.value
      result = value.send(operation, other.value)

      if result.is_a?(Numeric)
        Number.new(result, *compute_units(this, other, operation))
      else # Boolean op
        Bool.new(result)
      end
    end

    def coercion_factor(from_units, to_units)
      # get a list of unmatched units
      from_units, to_units = sans_common_units(from_units, to_units)

      if from_units.size != to_units.size || !convertable?(from_units | to_units)
        raise Sass::UnitConversionError.new(
          "Incompatible units: '#{from_units.join('*')}' and '#{to_units.join('*')}'.")
      end

      from_units.zip(to_units).inject(1) {|m, p| m * conversion_factor(p[0], p[1])}
    end

    def compute_units(this, other, operation)
      case operation
      when :*
        [this.numerator_units + other.numerator_units,
         this.denominator_units + other.denominator_units]
      when :/
        [this.numerator_units + other.denominator_units,
         this.denominator_units + other.numerator_units]
      else
        [this.numerator_units, this.denominator_units]
      end
    end

    def normalize!
      return if unitless?
      @numerator_units, @denominator_units =
        sans_common_units(@numerator_units, @denominator_units)

      @denominator_units.each_with_index do |d, i|
        next unless convertable?(d) && (u = @numerator_units.find {|n| convertable?([n, d])})
        @value /= conversion_factor(d, u)
        @denominator_units.delete_at(i)
        @numerator_units.delete_at(@numerator_units.index(u))
      end
    end

    # This is the source data for all the unit logic. It's pre-processed to make
    # it efficient to figure out whether a set of units is mutually compatible
    # and what the conversion ratio is between two units.
    #
    # These come from http://www.w3.org/TR/2012/WD-css3-values-20120308/.
    relative_sizes = [
      {
        'in' => Rational(1),
        'cm' => Rational(1, 2.54),
        'pc' => Rational(1, 6),
        'mm' => Rational(1, 25.4),
        'q' => Rational(1, 101.6),
        'pt' => Rational(1, 72),
        'px' => Rational(1, 96)
      },
      {
        'deg'  => Rational(1, 360),
        'grad' => Rational(1, 400),
        'rad'  => Rational(1, 2 * Math::PI),
        'turn' => Rational(1)
      },
      {
        's'  => Rational(1),
        'ms' => Rational(1, 1000)
      },
      {
        'Hz'  => Rational(1),
        'kHz' => Rational(1000)
      },
      {
        'dpi'  => Rational(1),
        'dpcm' => Rational(254, 100),
        'dppx' => Rational(96)
      }
    ]

    # A hash from each known unit to the set of units that it's mutually
    # convertible with.
    MUTUALLY_CONVERTIBLE = {}
    relative_sizes.map do |values|
      set = values.keys.to_set
      values.keys.each {|name| MUTUALLY_CONVERTIBLE[name] = set}
    end

    # A two-dimensional hash from two units to the conversion ratio between
    # them. Multiply `X` by `CONVERSION_TABLE[X][Y]` to convert it to `Y`.
    CONVERSION_TABLE = {}
    relative_sizes.each do |values|
      values.each do |(name1, value1)|
        CONVERSION_TABLE[name1] ||= {}
        values.each do |(name2, value2)|
          value = value1 / value2
          CONVERSION_TABLE[name1][name2] = value.denominator == 1 ? value.to_i : value.to_f
        end
      end
    end

    def conversion_factor(from_unit, to_unit)
      CONVERSION_TABLE[from_unit][to_unit]
    end

    def convertable?(units)
      units = Array(units).to_set
      return true if units.empty?
      return false unless (mutually_convertible = MUTUALLY_CONVERTIBLE[units.first])
      units.subset?(mutually_convertible)
    end

    def sans_common_units(units1, units2)
      units2 = units2.dup
      # Can't just use -, because we want px*px to coerce properly to px*mm
      units1 = units1.map do |u|
        j = units2.index(u)
        next u unless j
        units2.delete_at(j)
        nil
      end
      units1.compact!
      return units1, units2
    end
  end
end
