/**
 * Copyright (C) 2009 - present by OpenGamma Inc. and the OpenGamma group of companies
 *
 * Please see distribution for license.
 */
package com.opengamma.strata.math.impl;

import com.opengamma.strata.collect.ArgChecker;

/**
 * Utilities for working with complex numbers.
 */
public class ComplexMathUtils {

  public static ComplexNumber add(ComplexNumber z1, ComplexNumber z2) {
    ArgChecker.notNull(z1, "z1");
    ArgChecker.notNull(z2, "z2");
    return new ComplexNumber(z1.getReal() + z2.getReal(), z1.getImaginary() + z2.getImaginary());
  }

  public static ComplexNumber add(ComplexNumber... z) {
    ArgChecker.notNull(z, "z");
    double res = 0.0;
    double img = 0.0;
    for (ComplexNumber aZ : z) {
      res += aZ.getReal();
      img += aZ.getImaginary();
    }
    return new ComplexNumber(res, img);
  }

  public static ComplexNumber add(ComplexNumber z, double x) {
    ArgChecker.notNull(z, "z");
    return new ComplexNumber(z.getReal() + x, z.getImaginary());
  }

  public static ComplexNumber add(double x, ComplexNumber z) {
    ArgChecker.notNull(z, "z");
    return new ComplexNumber(z.getReal() + x, z.getImaginary());
  }

  public static double arg(ComplexNumber z) {
    ArgChecker.notNull(z, "z");
    return Math.atan2(z.getImaginary(), z.getReal());
  }

  public static ComplexNumber conjugate(ComplexNumber z) {
    ArgChecker.notNull(z, "z");
    return new ComplexNumber(z.getReal(), -z.getImaginary());
  }

  public static ComplexNumber divide(ComplexNumber z1, ComplexNumber z2) {
    ArgChecker.notNull(z1, "z1");
    ArgChecker.notNull(z2, "z2");
    double a = z1.getReal();
    double b = z1.getImaginary();
    double c = z2.getReal();
    double d = z2.getImaginary();
    if (Math.abs(c) > Math.abs(d)) {
      double dOverC = d / c;
      double denom = c + d * dOverC;
      return new ComplexNumber((a + b * dOverC) / denom, (b - a * dOverC) / denom);
    }
    double cOverD = c / d;
    double denom = c * cOverD + d;
    return new ComplexNumber((a * cOverD + b) / denom, (b * cOverD - a) / denom);
  }

  public static ComplexNumber divide(ComplexNumber z, double x) {
    ArgChecker.notNull(z, "z");
    return new ComplexNumber(z.getReal() / x, z.getImaginary() / x);
  }

  public static ComplexNumber divide(double x, ComplexNumber z) {
    ArgChecker.notNull(z, "z");
    double c = z.getReal();
    double d = z.getImaginary();
    if (Math.abs(c) > Math.abs(d)) {
      double dOverC = d / c;
      double denom = c + d * dOverC;
      return new ComplexNumber(x / denom, -x * dOverC / denom);
    }
    double cOverD = c / d;
    double denom = c * cOverD + d;
    return new ComplexNumber(x * cOverD / denom, -x / denom);
  }

  public static ComplexNumber exp(ComplexNumber z) {
    ArgChecker.notNull(z, "z");
    double mult = Math.exp(z.getReal());
    return new ComplexNumber(mult * Math.cos(z.getImaginary()), mult * Math.sin(z.getImaginary()));
  }

  public static ComplexNumber inverse(ComplexNumber z) {
    ArgChecker.notNull(z, "z");
    double c = z.getReal();
    double d = z.getImaginary();
    if (Math.abs(c) > Math.abs(d)) {
      double dOverC = d / c;
      double denom = c + d * dOverC;
      return new ComplexNumber(1 / denom, -dOverC / denom);
    }
    double cOverD = c / d;
    double denom = c * cOverD + d;
    return new ComplexNumber(cOverD / denom, -1 / denom);
  }

  /**
   * Returns the principal value of log, with z the principal argument of z defined to lie in the interval (-pi, pi]
   * @param z ComplexNumber
   * @return The log
   */
  public static ComplexNumber log(ComplexNumber z) {
    ArgChecker.notNull(z, "z");
    return new ComplexNumber(Math.log(Math.hypot(z.getReal(), z.getImaginary())), Math.atan2(z.getImaginary(), z.getReal()));
  }

  public static double mod(ComplexNumber z) {
    ArgChecker.notNull(z, "z");
    return Math.hypot(z.getReal(), z.getImaginary());
  }

  public static ComplexNumber square(ComplexNumber z) {
    ArgChecker.notNull(z, "z");
    double a = z.getReal();
    double b = z.getImaginary();
    return new ComplexNumber(a * a - b * b, 2 * a * b);
  }

  public static ComplexNumber multiply(ComplexNumber z1, ComplexNumber z2) {
    ArgChecker.notNull(z1, "z1");
    ArgChecker.notNull(z2, "z2");
    double a = z1.getReal();
    double b = z1.getImaginary();
    double c = z2.getReal();
    double d = z2.getImaginary();
    return new ComplexNumber(a * c - b * d, a * d + b * c);
  }

  public static ComplexNumber multiply(ComplexNumber... z) {
    ArgChecker.notNull(z, "z");
    int n = z.length;
    ArgChecker.isTrue(n > 0, "nothing to multiply");
    if (n == 1) {
      return z[0];
    } else if (n == 2) {
      return multiply(z[0], z[1]);
    } else {
      ComplexNumber product = multiply(z[0], z[1]);
      for (int i = 2; i < n; i++) {
        product = multiply(product, z[i]);
      }
      return product;
    }
  }

  public static ComplexNumber multiply(double x, ComplexNumber... z) {
    ComplexNumber product = multiply(z);
    return multiply(x, product);
  }

  public static ComplexNumber multiply(ComplexNumber z, double x) {
    ArgChecker.notNull(z, "z");
    return new ComplexNumber(z.getReal() * x, z.getImaginary() * x);
  }

  public static ComplexNumber multiply(double x, ComplexNumber z) {
    ArgChecker.notNull(z, "z");
    return new ComplexNumber(z.getReal() * x, z.getImaginary() * x);
  }

  public static ComplexNumber pow(ComplexNumber z1, ComplexNumber z2) {
    ArgChecker.notNull(z1, "z1");
    ArgChecker.notNull(z2, "z2");
    double mod = mod(z1);
    double arg = arg(z1);
    double mult = Math.pow(mod, z2.getReal()) * Math.exp(-z2.getImaginary() * arg);
    double theta = z2.getReal() * arg + z2.getImaginary() * Math.log(mod);
    return new ComplexNumber(mult * Math.cos(theta), mult * Math.sin(theta));
  }

  public static ComplexNumber pow(ComplexNumber z, double x) {
    double mod = mod(z);
    double arg = arg(z);
    double mult = Math.pow(mod, x);
    return new ComplexNumber(mult * Math.cos(x * arg), mult * Math.sin(x * arg));
  }

  public static ComplexNumber pow(double x, ComplexNumber z) {
    ArgChecker.notNull(z, "z");
    return pow(new ComplexNumber(x, 0), z);
  }

  public static ComplexNumber sqrt(ComplexNumber z) {
    ArgChecker.notNull(z, "z");
    double c = z.getReal();
    double d = z.getImaginary();
    if (c == 0.0 && d == 0.0) {
      return z;
    }
    double w;
    if (Math.abs(c) > Math.abs(d)) {
      double dOverC = d / c;
      w = Math.sqrt(Math.abs(c)) * Math.sqrt((1 + Math.sqrt(1 + dOverC * dOverC)) / 2);
    } else {
      double cOverD = c / d;
      w = Math.sqrt(Math.abs(d)) * Math.sqrt((Math.abs(cOverD) + Math.sqrt(1 + cOverD * cOverD)) / 2);
    }
    if (c >= 0.0) {
      return new ComplexNumber(w, d / 2 / w);
    }
    if (d >= 0.0) {
      return new ComplexNumber(d / 2 / w, w);
    }
    return new ComplexNumber(-d / 2 / w, -w);
  }

  public static ComplexNumber subtract(ComplexNumber z1, ComplexNumber z2) {
    ArgChecker.notNull(z1, "z1");
    ArgChecker.notNull(z2, "z2");
    return new ComplexNumber(z1.getReal() - z2.getReal(), z1.getImaginary() - z2.getImaginary());
  }

  public static ComplexNumber subtract(ComplexNumber z, double x) {
    ArgChecker.notNull(z, "z");
    return new ComplexNumber(z.getReal() - x, z.getImaginary());
  }

  public static ComplexNumber subtract(double x, ComplexNumber z) {
    ArgChecker.notNull(z, "z");
    return new ComplexNumber(x - z.getReal(), -z.getImaginary());
  }

}
