module UnitConversion
  class ValueUnit

    def self.for(value_string, unit_string = nil, formatter = nil)
      raise UnparseableUnitError, "value is empty" if value_string.blank?

      if ValueUnit === value_string
        return value_string
      end

      if String === value_string && unit_string.nil?
        value_string, unit_string = parse_single_string(value_string)
      end

      if value_string.is_a?(ParsedNumber)
        value = value_string
      else
        value =  ParsedNumber.new(value_string)
      end

      unit = case unit_string
        when nil
          nil
        when ->(u) { u.blank? }
          nil
        when ParsedUnit
          unit_string
        else
          ParsedUnit.new(unit_string)
      end

      if unit.nil?
        ValueNoUnit.new(value, formatter)
      else
        ValueUnit.new(value, unit, formatter)
      end
    end

    def self.parse_single_string(value_unit_string)
      match = UNIT_PARSING_REGEX.match(value_unit_string.to_s.strip)

      if match && match[:value].present?
        return match[:value], match[:unit]
      else
        raise UnparseableUnitError, "'#{value_unit_string}' does not appear to be a valid measurement of the form <value> <units> (ie '5 cup' or '223 gram/cup')"
      end
    end

    attr_reader :formatter

    def initialize(value, unit, formatter = nil)
      @value = value
      @unit = unit
      @formatter = formatter || NumberFormatter.for(value, unit)
    end

    def to_s
      "#{pretty_value} #{unit}"
    end

    def unit
      @unit
    end

    def value
      @value
    end

    def raw_value
      @value.value
    end

    def unitwise
      unit.unitwise(value.value)
    end

    def change_formatter(formatter)
      @formatter = formatter
      self
    end

    # Returns a new ValueUnit scaled by the given factor
    def scale(factor)
      if factor.present?
        parsed_factor = ParsedNumber.new(factor)
        ScaleConversion.new(parsed_factor).convert(self)
      else
        self
      end
    end

    # Returns a new ValueUnit with the given new_parsed_unit.  If converting between mass and volume,
    # also requires a density UnitValue
    def convert(new_unit, density = nil)
      new_parsed_unit = ParsedUnit.new(new_unit)
      parsed_density = density ? ValueUnit.for(density) : nil

      if new_parsed_unit.unit != self.unit.unit
        ConvertConversion.new(new_parsed_unit, parsed_density).convert(self)
      else
        self
      end
    end

    def auto_unit
      AutoUnitConversion.new.convert(self)
    end

    def to_metric
      MetricUnitConversion.new.convert(self)
    end

    def to_standard
      StandardUnitConversion.new.convert(self)
    end

    def to_mass(density)
      parsed_density = ValueUnit.for(density)
      MassUnitConversion.new(parsed_density).convert(self)
    end

    def to_volume(density)
      parsed_density = ValueUnit.for(density)
      VolumeUnitConversion.new(parsed_density).convert(self)
    end

    def density?
      unit.density?
    end

    def volume?
      unit.volume?
    end

    def mass?
      unit.mass?
    end

    def pretty_value
      formatter.format(value.value)
    end
  end

  class ValueNoUnit < ValueUnit
    def initialize(value, formatter = nil)
      @value = value
      @formatter = formatter || NumberFormatter.for(value, unit)
    end

    def to_s
      "#{pretty_value}"
    end

    def unit
      nil
    end

    def unitwise
      raise UnknownUnitError, "No unit value provided"
    end

    def convert(new_parsed_unit, density_unit_value = nil)
      raise UnknownUnitError, "No unit value provided"
    end

    def density?
      false
    end

    def volume?
      false
    end

    def mass?
      false
    end
  end
end