require "spec"
require "complex"
require "../support/number"
{% unless flag?(:wasm32) %}
  require "big"
{% end %}

# exact equality, including component signs
private def assert_complex_eq(z1 : Complex, z2 : Complex, *, file = __FILE__, line = __LINE__)
  z1.should eq(z2), file: file, line: line
  z1.real.sign_bit.should eq(z2.real.sign_bit), file: file, line: line
  z1.imag.sign_bit.should eq(z2.imag.sign_bit), file: file, line: line
end

private def assert_complex_nan(z : Complex, *, file = __FILE__, line = __LINE__)
  z.real.nan?.should be_true, file: file, line: line
  z.imag.nan?.should be_true, file: file, line: line
end

describe "Complex" do
  describe "as numbers" do
    it_can_convert_between([Complex], [Complex])
    it_can_convert_between({{BUILTIN_NUMBER_TYPES}}, [Complex])
    it_can_convert_between([Complex], {{BUILTIN_NUMBER_TYPES}})

    division_between_returns {{BUILTIN_NUMBER_TYPES}}, [Complex], Complex
    division_between_returns [Complex], {{BUILTIN_NUMBER_TYPES}}, Complex

    division_between_returns [Complex], [Complex], Complex
  end

  it "i" do
    a = 4.5 + 6.7.i
    b = Complex.new(4.5, 6.7)
    c = Complex.new(4.5, 9.6)
    a.should eq(b)
    a.should_not eq(c)
  end

  describe "==" do
    it "complex == complex" do
      a = Complex.new(1.5, 2)
      b = Complex.new(1.5, 2)
      c = Complex.new(2.25, 3)
      (a == b).should be_true
      (a == c).should be_false
    end

    it "complex == number" do
      a = Complex.new(5.3, 0)
      b = 5.3
      c = 4.2
      (a == b).should be_true
      (a == c).should be_false

      {% unless flag?(:wasm32) %}
        (a == BigDecimal.new(53, 1)).should be_false
      {% end %}
    end

    it "number == complex" do
      a = -1.75
      b = Complex.new(-1.75, 0)
      c = Complex.new(7.2, 0)
      (a == b).should be_true
      (a == c).should be_false

      {% unless flag?(:wasm32) %}
        (BigDecimal.new(72, 1) == c).should be_false
      {% end %}
    end
  end

  it "to_s" do
    Complex.new(1.25, 8.2).to_s.should eq("1.25 + 8.2i")
    Complex.new(1.25, -8.2).to_s.should eq("1.25 - 8.2i")

    Complex.new(+0.0, +0.0).to_s.should eq("0.0 + 0.0i")
    Complex.new(-0.0, -0.0).to_s.should eq("-0.0 - 0.0i")

    Complex.new(+Float64::INFINITY, +Float64::INFINITY).to_s.should eq("Infinity + Infinityi")
    Complex.new(-Float64::INFINITY, -Float64::INFINITY).to_s.should eq("-Infinity - Infinityi")

    pos_nan = Math.copysign(Float64::NAN, 1)
    neg_nan = Math.copysign(Float64::NAN, -1)
    Complex.new(pos_nan, pos_nan).to_s.should eq("NaN + NaNi")
    Complex.new(neg_nan, neg_nan).to_s.should eq("NaN + NaNi")
  end

  it "inspect" do
    Complex.new(1.25, 8.2).inspect.should eq("(1.25 + 8.2i)")
    Complex.new(1.25, -8.2).inspect.should eq("(1.25 - 8.2i)")

    Complex.new(+0.0, +0.0).inspect.should eq("(0.0 + 0.0i)")
    Complex.new(-0.0, -0.0).inspect.should eq("(-0.0 - 0.0i)")

    Complex.new(+Float64::INFINITY, +Float64::INFINITY).inspect.should eq("(Infinity + Infinityi)")
    Complex.new(-Float64::INFINITY, -Float64::INFINITY).inspect.should eq("(-Infinity - Infinityi)")

    pos_nan = Math.copysign(Float64::NAN, 1)
    neg_nan = Math.copysign(Float64::NAN, -1)
    Complex.new(pos_nan, pos_nan).inspect.should eq("(NaN + NaNi)")
    Complex.new(neg_nan, neg_nan).inspect.should eq("(NaN + NaNi)")
  end

  it "abs" do
    Complex.new(5.1, 9.7).abs.should eq(10.959014554237985)
  end

  it "abs2" do
    Complex.new(-1.1, 9).abs2.should eq(82.21)
  end

  describe "sign" do
    it "finite, non-zero" do
      Complex.new(-1.4, 7.7).sign.should be_close(Complex.new(-0.17888543819998315, 0.9838699100999074), 1e-14)
      Complex.new(1.4, -7.7).sign.should be_close(Complex.new(0.17888543819998315, -0.9838699100999074), 1e-14)
    end

    it "complex zero" do
      assert_complex_eq Complex.new(+0.0, +0.0).sign, Complex.new(+0.0, +0.0)
      assert_complex_eq Complex.new(+0.0, -0.0).sign, Complex.new(+0.0, -0.0)
      assert_complex_eq Complex.new(-0.0, +0.0).sign, Complex.new(-0.0, +0.0)
      assert_complex_eq Complex.new(-0.0, -0.0).sign, Complex.new(-0.0, -0.0)
    end

    it "real zero" do
      assert_complex_eq Complex.new(+0.0, +2.0).sign, Complex.new(+0.0, +1.0)
      assert_complex_eq Complex.new(+0.0, -2.0).sign, Complex.new(+0.0, -1.0)
      assert_complex_eq Complex.new(-0.0, +2.0).sign, Complex.new(-0.0, +1.0)
      assert_complex_eq Complex.new(-0.0, -2.0).sign, Complex.new(-0.0, -1.0)
    end

    it "imaginary zero" do
      assert_complex_eq Complex.new(+2.0, +0.0).sign, Complex.new(+1.0, +0.0)
      assert_complex_eq Complex.new(+2.0, -0.0).sign, Complex.new(+1.0, -0.0)
      assert_complex_eq Complex.new(-2.0, +0.0).sign, Complex.new(-1.0, +0.0)
      assert_complex_eq Complex.new(-2.0, -0.0).sign, Complex.new(-1.0, -0.0)
    end

    it "infinity" do
      inf = Float64::INFINITY

      # 1st quadrant
      assert_complex_eq Complex.new(+inf, +0.0).sign, Complex.new(+1.0, +0.0)
      assert_complex_eq Complex.new(+inf, +1.0).sign, Complex.new(+1.0, +0.0)
      assert_complex_eq Complex.new(+1.0, +inf).sign, Complex.new(+0.0, +1.0)
      assert_complex_eq Complex.new(+0.0, +inf).sign, Complex.new(+0.0, +1.0)

      # 2nd quadrant
      assert_complex_eq Complex.new(-0.0, +inf).sign, Complex.new(-0.0, +1.0)
      assert_complex_eq Complex.new(-1.0, +inf).sign, Complex.new(-0.0, +1.0)
      assert_complex_eq Complex.new(-inf, +1.0).sign, Complex.new(-1.0, +0.0)
      assert_complex_eq Complex.new(-inf, +0.0).sign, Complex.new(-1.0, +0.0)

      # 3rd quadrant
      assert_complex_eq Complex.new(-inf, -0.0).sign, Complex.new(-1.0, -0.0)
      assert_complex_eq Complex.new(-inf, -1.0).sign, Complex.new(-1.0, -0.0)
      assert_complex_eq Complex.new(-1.0, -inf).sign, Complex.new(-0.0, -1.0)
      assert_complex_eq Complex.new(-0.0, -inf).sign, Complex.new(-0.0, -1.0)

      # 4th quadrant
      assert_complex_eq Complex.new(+0.0, -inf).sign, Complex.new(+0.0, -1.0)
      assert_complex_eq Complex.new(+1.0, -inf).sign, Complex.new(+0.0, -1.0)
      assert_complex_eq Complex.new(+inf, -1.0).sign, Complex.new(+1.0, -0.0)
      assert_complex_eq Complex.new(+inf, -0.0).sign, Complex.new(+1.0, -0.0)

      # diagonals
      sqr = Math.sqrt(0.5)
      Complex.new(+inf, +inf).sign.should be_close(Complex.new(+sqr, +sqr), 1e-14)
      Complex.new(-inf, +inf).sign.should be_close(Complex.new(-sqr, +sqr), 1e-14)
      Complex.new(-inf, -inf).sign.should be_close(Complex.new(-sqr, -sqr), 1e-14)
      Complex.new(+inf, -inf).sign.should be_close(Complex.new(+sqr, -sqr), 1e-14)
    end

    it "not-a-number" do
      assert_complex_nan Complex.new(Float64::NAN, +0.0).sign
      assert_complex_nan Complex.new(Float64::NAN, +1.0).sign
      assert_complex_nan Complex.new(Float64::NAN, Float64::INFINITY).sign
      assert_complex_nan Complex.new(-0.0, Float64::NAN).sign
      assert_complex_nan Complex.new(-1.0, Float64::NAN).sign
      assert_complex_nan Complex.new(-Float64::INFINITY, Float64::NAN).sign
      assert_complex_nan Complex.new(Float64::NAN, Float64::NAN).sign
      assert_complex_nan Complex.new(Float64::NAN, Float64::NAN).sign
      assert_complex_nan Complex.new(Float64::NAN, Float64::NAN).sign
      assert_complex_nan Complex.new(Float64::NAN, Float64::NAN).sign
    end
  end

  it "phase" do
    Complex.new(11.5, -6.25).phase.should eq(-0.4978223326170012)
  end

  it "polar" do
    Complex.new(7.25, -13.1).polar.should eq({14.972391258579906, -1.0653196179316864})
  end

  it "cis" do
    {% if flag?(:aarch64) && flag?(:darwin) %}
      2.4.cis.should eq(Complex.new(-0.7373937155412454, 0.6754631805511511))
    {% else %}
      2.4.cis.should eq(Complex.new(-0.7373937155412454, 0.675463180551151))
    {% end %}
  end

  it "conj" do
    Complex.new(10.1, 3.7).conj.should eq(Complex.new(10.1, -3.7))
  end

  it "inv" do
    Complex.new(1.5, -2.5).inv.should eq(Complex.new(0.17647058823529413, 0.29411764705882354))
  end

  describe "+" do
    it "+ complex" do
      (+Complex.new(-5.43, -27.12)).should eq(Complex.new(-5.43, -27.12))
    end

    it "complex + complex" do
      (Complex.new(2.2, 7) + Complex.new(10.1, 1.34)).should eq(Complex.new(12.3, 8.34))
    end

    it "complex + number" do
      (Complex.new(0.3, 5.5) + 15).should eq(Complex.new(15.3, 5.5))
    end

    it "number + complex" do
      (-1.7 + Complex.new(7, 4.1)).should eq(Complex.new(5.3, 4.1))
    end
  end

  describe "-" do
    it "- complex" do
      (-Complex.new(5.43, 27.12)).should eq(Complex.new(-5.43, -27.12))
    end

    it "complex - complex" do
      (Complex.new(21.7, 2.0) - Complex.new(0.15, 3.4)).should eq(Complex.new(21.55, -1.4))
    end

    it "complex - number" do
      (Complex.new(8.1, 6.15) - 15).should eq(Complex.new(-6.9, 6.15))
    end

    it "number - complex" do
      (-3.27 - Complex.new(7, 5.1)).should eq(Complex.new(-10.27, -5.1))
    end
  end

  describe "*" do
    it "complex * complex" do
      (Complex.new(12.2, 9.8)*Complex.new(4.78, 2.86)).should eq(Complex.new(30.288, 81.736))
    end

    it "complex * number" do
      (Complex.new(11.3, 15.25)*1.2).should eq(Complex.new(13.56, 18.3))
    end

    it "number * complex" do
      (-1.7*Complex.new(9.7, 3.22)).should eq(Complex.new(-16.49, -5.474))
    end
  end

  describe "/" do
    it "complex / complex" do
      ((Complex.new(4, 6.2))/(Complex.new(0.5, 2.7))).should eq(Complex.new(2.485411140583554, -1.0212201591511936))
      ((Complex.new(4.1, 6.0))/(Complex.new(10, 2.2))).should eq(Complex.new(0.5169782525753529, 0.48626478443342236))

      (1.to_c / -1.to_c).should eq(-1.to_c)
      assert_complex_nan 1.to_c / Float64::NAN

      (1.to_c / 0.to_c).real.abs.should eq(Float64::INFINITY)
      (1.to_c / 0.to_c).imag.nan?.should be_true
    end

    it "complex / number" do
      ((Complex.new(21.3, 5.8))/1.9).should eq(Complex.new(11.210526315789474, 3.0526315789473686))
    end

    it "number / complex" do
      (-5.7/(Complex.new(2.27, 8.92))).should eq(Complex.new(-0.1527278908111847, 0.6001466017778712))
    end
  end

  it "clones" do
    c = Complex.new(4, 6.2)
    c.clone.should eq(c)
  end

  it "hashes real without imag like real only" do
    c = Complex.new(4, 0)
    c.hash.should eq(4_f64.hash)
  end

  it "test zero" do
    Complex.zero.should eq(Complex.new(0, 0))
  end

  it "test zero?" do
    Complex.new(0, 0).zero?.should be_true
    Complex.new(0, 3.4).zero?.should be_false
    Complex.new(1.2, 0).zero?.should be_false
    Complex.new(1.2, 3.4).zero?.should be_false
  end

  it "test additive_identity" do
    Complex.additive_identity.should eq(Complex.new(0, 0))
  end

  it "test multiplicative_identity" do
    Complex.multiplicative_identity.should eq(Complex.new(1, 0))
  end

  it "rounds" do
    (Complex.new(1.125, 0.875).round(2)).should eq(Complex.new(1.12, 0.88))
    (Complex.new(1.135, 0.865).round(2)).should eq(Complex.new(1.14, 0.86))
    (Complex.new(1.125, 0.875).round(digits: 1)).should eq(Complex.new(1.1, 0.9))
  end

  describe "Math" do
    it "exp" do
      Math.exp(Complex.new(1.15, -5.1)).should be_close(Complex.new(1.1937266270566773, 2.923901365414129), 1e-15)
    end

    it "log" do
      Math.log(Complex.new(1.25, -4.7)).should eq(Complex.new(1.5817344087982312, -1.3108561866063686))
    end

    it "log2" do
      Math.log2(Complex.new(-9.1, 3.2)).should eq(Complex.new(3.2699671225858946, +4.044523592551345))
    end

    it "log10" do
      Math.log10(Complex.new(2.11, 1.21)).should eq(Complex.new(0.38602142355392594, +0.22612668967405536))
    end

    it "sqrt" do
      Math.sqrt(Complex.new(1.32, 7.25)).should be_close(Complex.new(2.0843687106374236, 1.739135682425128), 1e-15)
      Math.sqrt(Complex.new(7.11, -0.9)).should be_close(Complex.new(2.671772413453534, -0.1684275194002508), 1e-15)
      Math.sqrt(Complex.new(-2.2, 6.22)).should be_close(Complex.new(1.4828360708935342, 2.0973323087062226), 1e-15)
      Math.sqrt(Complex.new(-8.3, -1.11)).should be_close(Complex.new(0.1922159681400434, -2.8873771797962275), 1e-15)
    end
  end
end
