module Unitwise
  # A Unit is essentially a collection of Unitwise::Term. Terms can be combined
  # through multiplication or division to create a unit. A unit does not have
  # a magnitude, but it does have a scale.
  class Unit
    liner :expression, :terms
    include Compatible

    # Create a new unit. You can send an expression or a collection of terms
    # @param input [String, Unit, [Term]] A string expression, a unit, or a
    # collection of tems.
    # @api public
    def initialize(input)
      case input
      when Compatible
        @expression = input.expression
      when String, Symbol
        @expression = input.to_s
      else
        @terms = input
      end
    end

    # The collection of terms used by this unit.
    # @return [Array]
    # @api public
    def terms
      unless frozen?
        unless @terms
          decomposer = Expression.decompose(@expression)
          @mode  = decomposer.mode
          @terms = decomposer.terms
        end
        freeze
      end
      @terms
    end

    # Build a string representation of this unit by it's terms.
    # @param mode [Symbol] The mode to use to stringify the atoms
    # (:primary_code, :names, :secondary_code).
    # @return [String]
    # @api public
    def expression(mode=nil)
      if @expression && (mode.nil? || mode == self.mode)
        @expression
      else
        Expression.compose(terms, mode || self.mode)
      end
    end

    # The collection of atoms that compose this unit. Essentially delegated to
    # terms.
    # @return [Array]
    # @api public
    def atoms
      terms.map(&:atom)
    end
    memoize :atoms

    # Is this unit special (meaning on a non-linear scale)?
    # @return [true, false]
    # @api public
    def special?
      terms.count == 1 && terms.all?(&:special?)
    end
    memoize :special?

    # A number representing this unit's distance from it's deepest terminal atom.
    # @return [Integer]
    # @api public
    def depth
      terms.map(&:depth).max + 1
    end
    memoize :depth

    # A collection of the deepest terms, or essential composition of the unit.
    # @return [Array]
    # @api public
    def root_terms
      terms.map(&:root_terms).flatten
    end
    memoize :root_terms

    # Get a scalar value for this unit.
    # @param magnitude [Numeric] An optional magnitude on this unit's scale.
    # @return [Numeric] A scalar value on a linear scale
    # @api public
    def scalar(magnitude = 1)
      terms.reduce(1) do |prod, term|
        prod * term.scalar(magnitude)
      end
    end

    # Get a magnitude for this unit based on a linear scale value.
    # Should only be used by units with special atoms in it's hierarchy.
    # @param scalar [Numeric] A linear scalar value
    # @return [Numeric] The equivalent magnitude on this scale
    # @api public
    def magnitude(scalar = scalar())
      terms.reduce(1) do |prod, term|
        prod * term.magnitude(scalar)
      end
    end

    # Multiply this unit by another unit, term, or number.
    # @param other [Unitwise::Unit, Unitwise::Term, Numeric]
    # @return [Unitwise::Unit]
    # @api public
    def *(other)
      operate('*', other) ||
        fail(TypeError, "Can't multiply #{ self } by #{ other }.")
    end

    # Divide this unit by another unit,term, or number.
    # @param other [Unitwise::Unit, Unitwise::Term, Numeric]
    # @return [Unitwise::Unit]
    # @api public
    def /(other)
      operate('/', other) ||
        fail(TypeError, "Can't divide #{ self } by #{ other }.")
    end


    # Raise this unit to a numeric power.
    # @param other [Numeric]
    # @return [Unitwise::Unit]
    # @api public
    def **(other)
      if other.is_a?(Numeric)
        self.class.new(terms.map { |t| t ** other })
      else
        fail TypeError, "Can't raise #{self} to #{other}."
      end
    end

    # A string representation of this unit.
    # @param mode [:symbol] The mode used to represent the unit
    # (:primary_code, :names, :secondary_code)
    # @return [String]
    # @api public
    def to_s(mode = nil)
      expression(mode)
    end

    # A collection of the possible string representations of this unit.
    # Primarily used by Unitwise::Search.
    # @return [Array]
    # @api public
    def aliases
      [:names, :primary_code, :secondary_code, :symbol].map do |mode|
        to_s(mode)
      end.uniq
    end
    memoize :aliases

    # The default mode to use for inspecting and printing.
    # @return [Symbol]
    # @api semipublic
    def mode
      terms
      @mode || :primary_code
    end

    private

    # Multiply or divide units
    # @api private
    def operate(operator, other)
      exp = operator == '/' ? -1 : 1
      if other.respond_to?(:terms)
        self.class.new(terms + other.terms.map { |t| t ** exp })
      elsif other.respond_to?(:atom)
        self.class.new(terms << other ** exp)
      elsif other.is_a?(Numeric)
        self.class.new(terms.map { |t| t.send(operator, other) })
      end
    end

  end
end
