require "spec/helpers/iterate"

# Helper methods to describe the behavior of numbers of different types
BUILTIN_NUMBER_TYPES =
  [Int8, UInt8, Int16, UInt16, Int32, UInt32, Int64, UInt64, Int128, UInt128, Float32, Float64]
BUILTIN_INTEGER_TYPES =
  [Int8, UInt8, Int16, UInt16, Int32, UInt32, Int64, UInt64, Int128, UInt128]
BUILTIN_INT_CONVERSIONS = {
  to_i: Int32, to_u: UInt32,
  to_i8: Int8, to_i16: Int16, to_i32: Int32, to_i64: Int64, to_i128: Int128,
  to_u8: UInt8, to_u16: UInt16, to_u32: UInt32, to_u64: UInt64, to_u128: UInt128,
}
BUILTIN_NUMBER_TYPES_LTE_64 =
  [Int8, UInt8, Int16, UInt16, Int32, UInt32, Int64, UInt64, Float32, Float64]
BUILTIN_FLOAT_TYPES =
  [Float32, Float64]
BUILTIN_NUMBER_SUFFIXES = {
  Int8    => "i8",
  Int16   => "i16",
  Int32   => "i32",
  Int64   => "i64",
  Int128  => "i128",
  UInt8   => "u8",
  UInt16  => "u16",
  UInt32  => "u32",
  UInt64  => "u64",
  UInt128 => "u128",
  Float32 => "f32",
  Float64 => "f64",
}

macro it_can_convert_between(a_types, b_types)
  {% for a_type in a_types %}
    {% for b_type in b_types %}
      it "converts from {{a_type}} to {{b_type}}" do
        {{b_type}}.new({{a_type}}.new(1)).should be_a({{b_type}})
      end

      it "converts from {{b_type}} to {{a_type}}" do
        {{a_type}}.new({{b_type}}.new(1)).should be_a({{a_type}})
      end
    {% end %}
  {% end %}
end

macro it_initializes_from_value_to(number_type)
  it "initialize from value to {{number_type}}" do
    {{number_type}}.new(1).should be_a({{number_type}})
    {{number_type}}.new(1).should eq(1)

    {{number_type}}.new(1u32).should be_a({{number_type}})
    {{number_type}}.new(1u32).should eq(1)

    {{number_type}}.new(1.0).should be_a({{number_type}})
    {{number_type}}.new(1.0).should eq(1)
  end
end

macro it_unchecked_initializes_from_value_to(number_type)
  it "unchecked initialize from value to {{number_type}}" do
    {{number_type}}.new!(1).should be_a({{number_type}})
    {{number_type}}.new!(1).should eq(1)

    {{number_type}}.new!(1u32).should be_a({{number_type}})
    {{number_type}}.new!(1u32).should eq(1)

    {{number_type}}.new!(1.0).should be_a({{number_type}})
    {{number_type}}.new!(1.0).should eq(1)
  end
end

macro division_between_returns(a_types, b_types, result_type)
  {% for a_type in a_types %}
    {% for b_type in b_types %}
      it "division between {{a_type}} / {{b_type}} returns {{result_type}}" do
        r = {{a_type}}.new(1) / {{b_type}}.new(1)
        r.should be_a({{result_type}})
        r.should eq(1)
      end
    {% end %}
  {% end %}
end

macro floor_division_returns_lhs_type(a_types, b_types)
  {% for a_type in a_types %}
    {% for b_type in b_types %}
      it "floor_division {{a_type}} // {{b_type}} returns {{a_type}}" do
        r = {{a_type}}.new(5) // {{b_type}}.new(2)
        r.should be_a({{a_type}})
        r.should eq(2)
      end
    {% end %}
  {% end %}
end

# TODO test to_X conversions return types
# TODO test zero? comparisons
# TODO test <=> comparisons between types

# Calls either `Float64.parse_hexfloat` or `Float32.parse_hexfloat`. The default
# is `Float64` unless *str* ends with `_f32`, in which case that suffix is
# stripped and `Float32` is chosen.
macro hexfloat(str)
  {% raise "`str` must be a StringLiteral, not #{str.class_name}" unless str.is_a?(StringLiteral) %}
  {% if str.ends_with?("_f32") %}
    ::Float32.parse_hexfloat({{ str[0...-4] }})
  {% else %}
    ::Float64.parse_hexfloat({{ str }})
  {% end %}
end

# See also: https://github.com/crystal-lang/crystal/issues/15192
lib LibC
  {% if flag?(:win32) %}
    FE_TONEAREST  = 0x00000000
    FE_DOWNWARD   = 0x00000100
    FE_UPWARD     = 0x00000200
    FE_TOWARDZERO = 0x00000300
  {% else %}
    FE_TONEAREST  = 0x00000000
    FE_DOWNWARD   = 0x00000400
    FE_UPWARD     = 0x00000800
    FE_TOWARDZERO = 0x00000C00
  {% end %}

  fun fegetround : Int
  fun fesetround(round : Int) : Int
end

def with_hardware_rounding_mode(mode, &)
  old_mode = LibC.fegetround
  LibC.fesetround(mode)
  yield ensure LibC.fesetround(old_mode)
end

def each_hardware_rounding_mode(&)
  {% for mode in %w(FE_TONEAREST FE_DOWNWARD FE_UPWARD FE_TOWARDZERO) %}
    with_hardware_rounding_mode(LibC::{{ mode.id }}) do
      yield LibC::{{ mode.id }}, {{ mode }}
    end
  {% end %}
end
