package jscl.math.numeric;

import jscl.AngleUnit;
import jscl.JsclMathEngine;
import jscl.math.NotDivisibleException;
import jscl.text.msg.JsclMessage;
import jscl.text.msg.Messages;
import org.solovyev.common.msg.MessageType;

import javax.annotation.Nonnull;

public final class Complex extends Numeric {

    @Nonnull
    public static final Complex I = new Complex(0, 1);
    private final double real, imaginary;

    private Complex(double real, double imaginary) {
        this.real = real;
        this.imaginary = imaginary;
    }

    @Nonnull
    public static Complex valueOf(double real, double imaginary) {
        if (JsclMathEngine.getInstance().getAngleUnits() != AngleUnit.rad) {
            JsclMathEngine.getInstance().getMessageRegistry().addMessage(new JsclMessage(Messages.msg_23, MessageType.warning));
        }

        if (real == 0d && imaginary == 1d) {
            return I;
        } else {
            return new Complex(real, imaginary);
        }
    }

    public Complex add(Complex complex) {
        return valueOf(real + complex.real, imaginary + complex.imaginary);
    }

    @Nonnull
    public Numeric add(@Nonnull Numeric that) {
        if (that instanceof Complex) {
            return add((Complex) that);
        } else if (that instanceof Real) {
            return add(valueOf(that));
        } else {
            return that.valueOf(this).add(that);
        }
    }

    public Complex subtract(Complex complex) {
        return valueOf(real - complex.real, imaginary - complex.imaginary);
    }

    @Nonnull
    public Numeric subtract(@Nonnull Numeric that) {
        if (that instanceof Complex) {
            return subtract((Complex) that);
        } else if (that instanceof Real) {
            return subtract(valueOf(that));
        } else {
            return that.valueOf(this).subtract(that);
        }
    }

    public Complex multiply(Complex complex) {
        return valueOf(real * complex.real - imaginary * complex.imaginary, real * complex.imaginary + imaginary * complex.real);
    }

    @Nonnull
    public Numeric multiply(@Nonnull Numeric that) {
        if (that instanceof Complex) {
            return multiply((Complex) that);
        } else if (that instanceof Real) {
            return multiply(valueOf(that));
        } else {
            return that.multiply(this);
        }
    }

    public Complex divide(Complex complex) throws ArithmeticException {
        return multiply((Complex) complex.inverse());
    }

    @Nonnull
    public Numeric divide(@Nonnull Numeric that) throws NotDivisibleException {
        if (that instanceof Complex) {
            return divide((Complex) that);
        } else if (that instanceof Real) {
            return divide(valueOf(that));
        } else {
            return that.valueOf(this).divide(that);
        }
    }

    @Nonnull
    public Numeric negate() {
        return valueOf(-real, -imaginary);
    }

    @Nonnull
    @Override
    public Numeric abs() {
        final Numeric realSquare = new Real(real).pow(2);
        final Numeric imaginarySquare = new Real(imaginary).pow(2);
        final Numeric sum = realSquare.add(imaginarySquare);
        return sum.sqrt();
    }

    public int signum() {
        int result;

        if (real > .0) {
            result = 1;
        } else if (real < .0) {
            result = -1;
        } else {
            result = Real.signum(imaginary);
        }

        return result;
    }

    public double magnitude() {
        return Math.sqrt(real * real + imaginary * imaginary);
    }

    public double magnitude2() {
        return real * real + imaginary * imaginary;
    }

    public double angle() {
        return Math.atan2(imaginary, real);
    }

    @Nonnull
    public Numeric ln() {
        if (signum() == 0) {
            return Real.ZERO.ln();
        } else {
            return valueOf(Math.log(magnitude()), angle());
        }
    }

    @Nonnull
    public Numeric lg() {
        if (signum() == 0) {
            return Real.ZERO.lg();
        } else {
            return valueOf(Math.log10(magnitude()), angle());
        }
    }

    @Nonnull
    public Numeric exp() {
        return valueOf(Math.cos(defaultToRad(imaginary)), Math.sin(defaultToRad(imaginary))).multiply(Math.exp(real));
    }

    @Nonnull
    public Numeric inverse() {
        return ((Complex) conjugate()).divide(magnitude2());
    }

    Complex multiply(double d) {
        return valueOf(real * d, imaginary * d);
    }

    Complex divide(double d) {
        return valueOf(real / d, imaginary / d);
    }

    public Numeric conjugate() {
        return valueOf(real, -imaginary);
    }

    public double realPart() {
        return real;
    }

    public double imaginaryPart() {
        return imaginary;
    }

    public int compareTo(Complex that) {
        if (imaginary < that.imaginary) {
            return -1;
        } else if (imaginary > that.imaginary) {
            return 1;
        } else if (imaginary == that.imaginary) {
            if (real < that.real) {
                return -1;
            } else if (real > that.real) {
                return 1;
            } else if (real == that.real) {
                return 0;
            } else throw new ArithmeticException();
        } else throw new ArithmeticException();
    }

    public int compareTo(Numeric that) {
        if (that instanceof Complex) {
            return compareTo((Complex) that);
        } else if (that instanceof Real) {
            return compareTo(valueOf(that));
        } else {
            return that.valueOf(this).compareTo(that);
        }
    }

    public Complex copyOf(@Nonnull Complex complex) {
        return valueOf(complex.real, complex.imaginary);
    }

    @Nonnull
    public Numeric valueOf(@Nonnull Numeric numeric) {
        if (numeric instanceof Complex) {
            return copyOf((Complex) numeric);
        } else if (numeric instanceof Real) {
            Real d = (Real) numeric;
            return d.toComplex();
        } else throw new ArithmeticException();
    }

    public String toString() {
        final StringBuilder result = new StringBuilder();

        if (imaginary == 0.) {
            result.append(toString(real));
        } else {
            if (real != 0.) {
                result.append(toString(real));
                if (imaginary > 0.) {
                    result.append("+");
                }
            }

            if (imaginary != 1.) {
                if (imaginary == -1.) {
                    result.append("-");
                } else {
                    if (imaginary < 0.) {
                        final String imagStr = toString(imaginary);
                        // due to rounding we can forget sign (-0.00000000001 can be round to 0 => plus sign would not be added above and no sign will be before i)
                        if (imagStr.startsWith("-")) {
                            result.append(imagStr);
                        } else {
                            result.append("-").append(imagStr);
                        }
                    } else {
                        result.append(toString(imaginary));
                    }
                    result.append("*");
                }
            }
            result.append("i");
        }

        return result.toString();
    }
}
