module UnitConversion

  class Conversion
  end

  class ScaleConversion < Conversion
    def initialize(parsed_factor)
      @factor = parsed_factor
    end

    def convert(value_unit)
      value = @factor.value * value_unit.value.value
      ValueUnit.for(value, value_unit.unit, value_unit.formatter)
    end
  end

  class ConvertConversion < Conversion
    def initialize(parsed_unit, density_unit_value = nil)
      @target_unit = parsed_unit

      raise UnknownUnitError, "#{density_unit_value} is not a density" if density_unit_value && !density_unit_value.density?
      @density = density_unit_value
    end

    def convert(value_unit)

      input = value_unit.unitwise * 1.0

      if value_unit.volume? && @target_unit.mass?
        raise MissingDensityError, "Cannot convert #{value_unit.unit} to #{@target_unit} without density" unless @density
        input = input * @density.unitwise
      elsif value_unit.mass? && @target_unit.volume?
        raise MissingDensityError, "Cannot convert #{value_unit.unit} to #{@target_unit} without density" unless @density
        input = input / @density.unitwise
      end

      begin
        input = input.convert_to @target_unit.unit
      rescue Unitwise::ConversionError, Unitwise::ExpressionError => err
        raise ConversionError, err.message
      end


      formatter = @target_unit.metric? ? DecimalFormatter.new : value_unit.formatter

      ValueUnit.for(input.value, @target_unit, formatter)

    end
  end

  class AutoUnitConversion < Conversion

    def convert(value_unit)

      if value_unit.unit.nil? || (!value_unit.mass? && !value_unit.volume?)
        return value_unit
      end

      unless known_auto_unit?(value_unit.unit)
        value_unit = value_unit.mass? ? value_unit.convert("g") : value_unit.convert("ml")
      end

      value = value_unit.raw_value
      unit = value_unit.unit.unit
      new_unit = unit

      unit_orders = UNIT_ORDERS.values.detect { |orders| orders.include?(unit.to_sym) }
      unit_range = UNIT_RANGES[unit.to_sym]

      if value < unit_range.first
        unit_orders = unit_orders.reverse
      end

      idx = unit_orders.index(new_unit.to_sym)

      while !unit_range.include?(value) && idx < (unit_orders.length - 1)
        idx += 1
        next_unit = unit_orders[idx]
        value = Unitwise(value, new_unit).convert_to(next_unit).value
        new_unit = next_unit.to_s
        unit_range = UNIT_RANGES[new_unit.to_sym]
      end

      ValueUnit.for(value, new_unit, value_unit.formatter)
    end

    def known_auto_unit?(unit)
      unit && UNIT_ORDERS.values.flatten.include?(unit.unit.to_sym)
    end
  end

  class MetricUnitConversion < Conversion
    def convert(value_unit)
      if value_unit.unit && !value_unit.unit.metric?
        if value_unit.mass?
          value_unit = value_unit.convert('g').auto_unit
        elsif value_unit.volume?
          value_unit = value_unit.convert('ml').auto_unit
        end
      end

      value_unit
    end
  end

  class StandardUnitConversion < Conversion
    def convert(value_unit)
      if value_unit.unit && value_unit.unit.metric?
        if value_unit.mass?
          value_unit = value_unit.convert('oz').auto_unit.change_formatter(RationalFormatter.new)
        elsif value_unit.volume?
          value_unit = value_unit.convert('cup').auto_unit.change_formatter(RationalFormatter.new)
        end
      end

      value_unit
    end
  end

  class VolumeUnitConversion < Conversion
    def initialize(density_unit_value)
      raise UnknownUnitError, "#{density_unit_value} is not a density" if !density_unit_value.density?
      @density = density_unit_value
    end

    def convert(value_unit)
      if value_unit.mass?
        unit = value_unit.unit.metric? ? 'ml' : 'cup'
        value_unit.convert(unit, @density).auto_unit
      else
        value_unit
      end
    end
  end

  class MassUnitConversion < Conversion
    def initialize(density_unit_value)
      raise UnknownUnitError, "#{density_unit_value} is not a density" if !density_unit_value.density?
      @density = density_unit_value
    end

    def convert(value_unit)
      if value_unit.volume?
        unit = value_unit.unit.metric? ? 'g' : 'oz'
        value_unit.convert(unit, @density).auto_unit
      else
        value_unit
      end
    end
  end

end
