# A complex number is a number represented in the form a + bi. In this form,
# a and b are real numbers, and i is an imaginary number such as i² = -1.
# The a is the real part of the number, and the b is the imaginary part of
# the number.
#
# NOTE: To use `Complex`, you must explicitly import it with `require "complex"`
#
# ```
# require "complex"
#
# Complex.new(1, 0)   # => 1.0 + 0.0.i
# Complex.new(5, -12) # => 5.0 - 12.0.i
#
# 1.to_c # => 1.0 + 0.0.i
# 1.i    # => 0.0 + 1.0.i
# ```
struct Complex
  # Returns the real part.
  getter real : Float64

  # Returns the imaginary part.
  getter imag : Float64

  def initialize(real : Number, imag : Number = 0)
    @real = real.to_f
    @imag = imag.to_f
  end

  def self.new(c : Complex)
    c
  end

  # Determines whether `self` equals *other* or not.
  def ==(other : Complex)
    @real == other.real && @imag == other.imag
  end

  # :ditto:
  def ==(other : Number)
    @real == other && @imag.zero?
  end

  # :ditto:
  def ==(other)
    false
  end

  # Returns `self`.
  def to_c
    self
  end

  # Returns the value as a `Float64` if possible (the imaginary part should be exactly zero),
  # raises otherwise.
  def to_f64 : Float64
    unless @imag.zero?
      raise Exception.new "Complex number with non-zero imaginary part can't be converted to real number"
    end
    @real
  end

  # See `#to_f64`.
  def to_f
    to_f64
  end

  delegate to_i128, to_i64, to_i32, to_i16, to_i8, to: to_f64
  delegate to_u128, to_u64, to_u32, to_u16, to_u8, to: to_f64
  delegate to_f32, to: to_f64

  # See `#to_i32`.
  def to_i
    to_i32
  end

  # Writes this complex object to an *io*.
  #
  # ```
  # require "complex"
  #
  # Complex.new(42, 2).to_s # => "42.0 + 2.0i"
  # ```
  def to_s(io : IO) : Nil
    io << @real
    io << (@imag.nan? || @imag.sign_bit > 0 ? " + " : " - ")
    io << Math.copysign(@imag, 1.0)
    io << 'i'
  end

  # Writes this complex object to an *io*, surrounded by parentheses.
  #
  # ```
  # require "complex"
  #
  # Complex.new(42, 2).inspect # => "(42.0 + 2.0i)"
  # ```
  def inspect(io : IO) : Nil
    io << '('
    to_s(io)
    io << ')'
  end

  # Returns the absolute value of this complex number in a
  # number form, using the Pythagorean theorem.
  #
  # ```
  # require "complex"
  #
  # Complex.new(42, 2).abs  # => 42.04759208325728
  # Complex.new(-42, 2).abs # => 42.04759208325728
  # ```
  def abs : Float64
    Math.hypot(@real, @imag)
  end

  # Returns the square of absolute value in a number form.
  #
  # ```
  # require "complex"
  #
  # Complex.new(42, 2).abs2 # => 1768
  # ```
  def abs2 : Float64
    @real * @real + @imag * @imag
  end

  # Returns the complex sign of `self`.
  #
  # If `self` is non-zero, the returned value has the same phase as `self` and
  # absolute value `1.0`. If `self` is zero, returns `self`.
  #
  # The returned value's real and imaginary components always have the same
  # signs as the respective components of `self`.
  #
  # ```
  # require "complex"
  #
  # Complex.new(7, -24).sign        # => (0.28 - 0.96.i)
  # Complex.new(1.0 / 0.0, 24).sign # => (1.0 + 0.0.i)
  # Complex.new(-0.0, +0.0).sign    # => (-0.0 + 0.0.i)
  # ```
  def sign : Complex
    return self if zero?

    if @real.nan? || @imag.nan?
      return Complex.new(Float64::NAN, Float64::NAN)
    end

    return Complex.new(@real.sign, @imag) if @real != 0 && @imag == 0
    return Complex.new(@real, @imag.sign) if @real == 0 && @imag != 0

    case {real_inf_sign = @real.infinite?, imag_inf_sign = @imag.infinite?}
    in {Nil, Nil}
      phase.cis
    in {Nil, Int32}
      Complex.new(Math.copysign(0.0, @real), imag_inf_sign)
    in {Int32, Nil}
      Complex.new(real_inf_sign, Math.copysign(0.0, @imag))
    in {Int32, Int32}
      sqrt = Math.sqrt(0.5)
      Complex.new(sqrt * real_inf_sign, sqrt * imag_inf_sign)
    end
  end

  # Returns the phase of `self`.
  def phase : Float64
    Math.atan2(@imag, @real)
  end

  # Returns a `Tuple` with the `abs` value and the `phase`.
  #
  # ```
  # require "complex"
  #
  # Complex.new(42, 2).polar # => {42.047592083257278, 0.047583103276983396}
  # ```
  def polar : {Float64, Float64}
    {abs, phase}
  end

  # Returns the conjugate of `self`.
  #
  # ```
  # require "complex"
  #
  # Complex.new(42, 2).conj  # => 42.0 - 2.0.i
  # Complex.new(42, -2).conj # => 42.0 + 2.0.i
  # ```
  def conj : Complex
    Complex.new(@real, -@imag)
  end

  # Returns the inverse of `self`.
  def inv : Complex
    conj / abs2
  end

  # Returns `self`.
  def + : Complex
    self
  end

  # Adds the value of `self` to *other*.
  def +(other : Complex) : Complex
    Complex.new(@real + other.real, @imag + other.imag)
  end

  # :ditto:
  def +(other : Number) : Complex
    Complex.new(@real + other, @imag)
  end

  # Returns the opposite of `self`.
  def - : Complex
    Complex.new(-@real, -@imag)
  end

  # Removes the value of *other* from `self`.
  def -(other : Complex) : Complex
    Complex.new(@real - other.real, @imag - other.imag)
  end

  # :ditto:
  def -(other : Number) : Complex
    Complex.new(@real - other, @imag)
  end

  # Multiplies `self` by *other*.
  def *(other : Complex) : Complex
    Complex.new(@real * other.real - @imag * other.imag, @real * other.imag + @imag * other.real)
  end

  # :ditto:
  def *(other : Number) : Complex
    Complex.new(@real * other, @imag * other)
  end

  # Divides `self` by *other*.
  def /(other : Complex) : Complex
    if other.real.nan? || other.imag.nan?
      Complex.new(Float64::NAN, Float64::NAN)
    elsif other.imag.abs < other.real.abs
      r = other.imag / other.real
      d = other.real + r * other.imag

      if d.nan? || d == 0
        Complex.new(Float64::NAN, Float64::NAN)
      else
        Complex.new((@real + @imag * r) / d, (@imag - @real * r) / d)
      end
    elsif other.imag == 0 # other.real == 0
      Complex.new(@real / other.real, @imag / other.real)
    else # 0 < other.real.abs <= other.imag.abs
      r = other.real / other.imag
      d = other.imag + r * other.real

      if d.nan? || d == 0
        Complex.new(Float64::NAN, Float64::NAN)
      else
        Complex.new((@real * r + @imag) / d, (@imag * r - @real) / d)
      end
    end
  end

  # :ditto:
  def /(other : Number) : Complex
    Complex.new(@real / other, @imag / other)
  end

  def clone
    self
  end

  # See `Object#hash(hasher)`
  def hash(hasher)
    hasher = real.hash(hasher)
    hasher = imag.hash(hasher) unless imag.zero?
    hasher
  end

  # Returns the number `0` in complex form.
  def self.zero : Complex
    new 0, 0
  end

  # Returns `true` if the complex number is zero.
  # This means the real and imaginary are both zero.
  #
  # ```
  # require "complex"
  #
  # Complex.new(0, 0).zero? # => true
  # Complex.new(1, 0).zero? # => false
  # Complex.new(0, 1).zero? # => false
  # ````
  def zero? : Bool
    @real == 0 && @imag == 0
  end

  def self.additive_identity : self
    zero
  end

  def self.multiplicative_identity : self
    new 1, 0
  end

  # Rounds to the nearest *digits*.
  def round(digits = 0) : Complex
    Complex.new(@real.round(digits), @imag.round(digits))
  end
end

struct Number
  # Returns a `Complex` object with the value of `self` as the real part.
  def to_c : Complex
    Complex.new(self, 0)
  end

  # Returns a `Complex` object with the value of `self` as the imaginary part.
  def i : Complex
    Complex.new(0, self)
  end

  def ==(other : Complex)
    other == self
  end

  # [Cis](https://en.wikipedia.org/wiki/Cis_(mathematics)) is a mathematical notation representing `cos x + i sin x`.
  #
  # Returns a `Complex` object with real part `Math.cos(self)` and imaginary part `Math.sin(self)`, where `self` represents the angle in radians.
  def cis : Complex
    Complex.new(Math.cos(self), Math.sin(self))
  end

  def +(other : Complex) : Complex
    Complex.new(self + other.real, other.imag)
  end

  def -(other : Complex) : Complex
    Complex.new(self - other.real, -other.imag)
  end

  def *(other : Complex) : Complex
    Complex.new(self * other.real, self * other.imag)
  end

  def /(other : Complex) : Complex
    self * other.inv
  end
end

module Math
  # Calculates the exponential of *value*.
  #
  # ```
  # require "complex"
  #
  # Math.exp(4 + 2.i) # => -22.720847417619233 + 49.645957334580565.i
  # ```
  def exp(value : Complex) : Complex
    r = exp(value.real)
    Complex.new(r * cos(value.imag), r * sin(value.imag))
  end

  # Calculates the natural logarithm of *value*.
  #
  # ```
  # require "complex"
  #
  # Math.log(4 + 2.i) # => 1.4978661367769956 + 0.4636476090008061.i
  # ```
  def log(value : Complex) : Complex
    Complex.new(Math.log(value.abs), value.phase)
  end

  # Calculates the logarithm of *value* to base 2.
  #
  # ```
  # require "complex"
  #
  # Math.log2(4 + 2.i) # => 2.1609640474436813 + 0.6689021062254881.i
  # ```
  def log2(value : Complex) : Complex
    log(value) / LOG2
  end

  # Calculates the logarithm of *value* to base 10.
  #
  # ```
  # require "complex"
  #
  # Math.log10(4 + 2.i) # => 0.6505149978319906 + 0.20135959813668655.i
  # ```
  def log10(value : Complex) : Complex
    log(value) / LOG10
  end

  # Calculates the square root of *value*.
  # Inspired by the [following blog post](https://pavpanchekha.com/blog/casio-mathjs.html) of Pavel Panchekha on floating point precision.
  #
  # ```
  # require "complex"
  #
  # Math.sqrt(4 + 2.i) # => 2.0581710272714924 + 0.48586827175664565.i
  # ```
  #
  # Although the imaginary number is defined as i = sqrt(-1),
  # calling `Math.sqrt` with a negative number will return `-NaN`.
  # To obtain the result in the complex plane, `Math.sqrt` must
  # be called with a complex number.
  #
  # ```
  # Math.sqrt(-1.0)         # => -NaN
  # Math.sqrt(-1.0 + 0.0.i) # => 0.0 + 1.0.i
  # ```
  def sqrt(value : Complex) : Complex
    r = value.abs

    re = if value.real >= 0
           0.5 * sqrt(2.0 * (r + value.real))
         else
           value.imag.abs / sqrt(2.0 * (r - value.real))
         end

    im = if value.real <= 0
           0.5 * sqrt(2.0 * (r - value.real))
         else
           value.imag.abs / sqrt(2.0 * (r + value.real))
         end

    Complex.new(re, value.imag >= 0 ? im : -im)
  end
end
