/*
 * Copyright (c) 1998-2006 Caucho Technology -- all rights reserved
 *
 * This file is part of Resin(R) Open Source
 *
 * Each copy or derived work must preserve the copyright notice and this
 * notice unmodified.
 *
 * Resin Open Source is free software; you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation; either version 2 of the License, or
 * (at your option) any later version.
 *
 * Resin Open Source is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE, or any warranty
 * of NON-INFRINGEMENT.  See the GNU General Public License for more
 * details.
 *
 * You should have received a copy of the GNU General Public License
 * along with Resin Open Source; if not, write to the
 *
 *   Free Software Foundation, Inc.
 *   59 Temple Place, Suite 330
 *   Boston, MA 02111-1307  USA
 *
 * @author Sam
 */

package com.caucho.quercus.lib;

import java.math.BigDecimal;
import java.math.RoundingMode;
import java.math.MathContext;
import java.math.BigInteger;
import java.util.HashMap;
import java.util.Map;

import com.caucho.util.L10N;

import com.caucho.quercus.module.AbstractQuercusModule;
import com.caucho.quercus.module.Optional;

import com.caucho.quercus.env.*;

/**
 * PHP math routines.
 */
public class BcmathModule extends AbstractQuercusModule {
  private static final L10N L = new L10N(BcmathModule.class);

  private static final BigDecimal ZERO = BigDecimal.ZERO;
  private static final BigDecimal ONE = BigDecimal.ONE;
  private static final BigDecimal TWO = new BigDecimal(2);
  private static final int SQRT_MAX_ITERATIONS = 50;

  private static final HashMap<String,StringValue> _iniMap
    = new HashMap<String,StringValue>();

  static {
    addIni(_iniMap, "bcmath.scale", "0", PHP_INI_ALL);
  }

  public String []getLoadedExtensions()
  {
    return new String[] {  "bcmath" };
  }

  public Map<String,StringValue> getDefaultIni()
  {
    return _iniMap;
  }

  private static BigDecimal toBigDecimal(Value value)
  {
    try {
      if (value instanceof StringValue)
        return new BigDecimal(value.toString());
      if (value instanceof DoubleValue)
        return new BigDecimal(value.toDouble());
      else if (value instanceof LongValue)
        return new BigDecimal(value.toLong());
      else
        return new BigDecimal(value.toString());
    }
    catch (NumberFormatException ex) {
      return ZERO;
    }
    catch (IllegalArgumentException ex) {
      return ZERO;
    }
  }

  private static int calculateScale(Env env, int scale)
  {
    if (scale < 0) {
      Value iniValue = env.getIni("bcmath.scale");

      if (iniValue != null)
        scale = iniValue.toInt();
    }

    if (scale < 0)
      scale = 0;

    return scale;
  }

  /**
   * Add two arbitrary precision numbers.
   *
   * The optional scale indicates the number of decimal digits to include in
   * the result, the default is the value of a previous call to {@link #bcscale}
   * or the value of the ini variable "bcmath.scale".
   */
  public static String bcadd(Env env, Value value1, Value value2, @Optional("-1") int scale)
  {
    scale = calculateScale(env, scale);

    BigDecimal bd1 = toBigDecimal(value1);
    BigDecimal bd2 = toBigDecimal(value2);

    BigDecimal bd = bd1.add(bd2);

    bd = bd.setScale(scale, RoundingMode.DOWN);

    return bd.toPlainString();
  }

  /**
   * Compare two arbitrary precision numbers, return -1 if value 1 < value2,
   * 0 if value1 == value2, 1 if value1 > value2.
   *
   * The optional scale indicates the number of decimal digits to include in
   * comparing the values, the default is the value of a previous call to
   * {@link #bcscale} or the value of the ini variable "bcmath.scale".
   */
  public static int bccomp(Env env, Value value1, Value value2, @Optional("-1") int scale)
  {
    scale = calculateScale(env, scale);

    BigDecimal bd1 = toBigDecimal(value1);
    BigDecimal bd2 = toBigDecimal(value2);

    bd1 = bd1.setScale(scale, RoundingMode.DOWN);
    bd2 = bd2.setScale(scale, RoundingMode.DOWN);

    return bd1.compareTo(bd2);
  }

  /**
   * Divide one arbitrary precision number (value1) by another (value2).
   *
   * A division by zero results in a warning message and a return value of null.
   *
   * The optional scale indicates the number of decimal digits to include in
   * the result, the default is the value of a previous call to {@link #bcscale}
   * or the value of the ini variable "bcmath.scale".
   */
  public static String bcdiv(Env env, Value value1, Value value2, @Optional("-1") int scale)
  {
    scale = calculateScale(env, scale);

    BigDecimal bd1 = toBigDecimal(value1);
    BigDecimal bd2 = toBigDecimal(value2);

    if (bd2.compareTo(ZERO) == 0) {
      env.warning(L.l("division by zero"));
      return null;
    }

    BigDecimal result;

    if (scale > 0) {
      result = bd1.divide(bd2, scale + 2, RoundingMode.DOWN);
    }
    else {
      result = bd1.divide(bd2, 2, RoundingMode.DOWN);
    }

    result = result.setScale(scale, RoundingMode.DOWN);

    return result.toPlainString();
  }

  /**
   * Return the modulus of an aribtrary precison number.
   * The returned number is always a whole number.
   *
   * A modulus of 0 results in a division by zero warning message and a
   * return value of null.
   */
  public static String bcmod(Env env, Value value, Value modulus)
  {
    BigDecimal bd1 = toBigDecimal(value).setScale(0, RoundingMode.DOWN);
    BigDecimal bd2 = toBigDecimal(modulus).setScale(0, RoundingMode.DOWN);

    if (bd2.compareTo(ZERO) == 0) {
      env.warning(L.l("division by zero"));
      return null;
    }

    BigDecimal bd = bd1.remainder(bd2, MathContext.DECIMAL128);

    // scale is always 0 in php
    bd = bd.setScale(0, RoundingMode.DOWN);

    return bd.toPlainString();
  }

  /**
   * Multiply two arbitrary precision numbers.
   *
   * The optional scale indicates the number of decimal digits to include in
   * the result, the default is the value of a previous call to {@link #bcscale}
   * or the value of the ini variable "bcmath.scale".
   */
  public static String bcmul(Env env, Value value1, Value value2, @Optional("-1") int scale)
  {
    scale = calculateScale(env, scale);

    BigDecimal bd1 = toBigDecimal(value1);
    BigDecimal bd2 = toBigDecimal(value2);

    BigDecimal bd = bd1.multiply(bd2);

    // odd php special case for 0, scale is ignored:
    if (bd.compareTo(ZERO) == 0) {
      if (scale > 0)
        return "0.0";
      else
        return "0";
    }

    bd = bd.setScale(scale, RoundingMode.DOWN);
    bd = bd.stripTrailingZeros();

    return bd.toPlainString();
  }

  /**
   * Raise one arbitrary precision number (base) to the power of another (exp).
   *
   * exp must be a whole number. Negative exp is supported.
   *
   * The optional scale indicates the number of decimal digits to include in
   * the result, the default is the value of a previous call to {@link #bcscale}
   * or the value of the ini variable "bcmath.scale".
   */
  public static String bcpow(Env env, Value base, Value exp, @Optional("-1") int scale)
  {
    scale = calculateScale(env, scale);

    BigDecimal bd1 = toBigDecimal(base);
    BigDecimal bd2 = toBigDecimal(exp);

    if (bd2.scale() > 0)
      env.warning("fractional exponent not supported");

    int exponent = bd2.toBigInteger().intValue();

    if (exponent == 0)
      return "1";

    boolean isNeg;

    if (exponent < 0)  {
      isNeg = true;
      exponent *= -1;
    }
    else
      isNeg = false;

    BigDecimal bd = bd1.pow(exponent);

    if (isNeg)
      bd = ONE.divide(bd, scale + 2, RoundingMode.DOWN);

    bd = bd.setScale(scale, RoundingMode.DOWN);

    if (bd.compareTo(BigDecimal.ZERO) == 0)
      return "0";

    bd = bd.stripTrailingZeros();

    return bd.toPlainString();
  }

  /**
   * Raise one arbitrary precision number (base) to the power of another (exp),
   * and then return the modulus.
   * The returned number is always a whole number.
   *
   * exp must be a whole number. Negative exp is supported.
   *
   * The optional scale indicates the number of decimal digits to include in
   * the pow calculation, the default is the value of a previous call to {@link #bcscale}
   * or the value of the ini variable "bcmath.scale".
   */
  public static String bcpowmod(Env env, Value base, Value exp, Value modulus, @Optional("-1") int scale)
  {
    scale = calculateScale(env, scale);

    // XXX: this is inefficient, s/b fast-exponentiation
    String pow = bcpow(env, base, exp, scale);

    if (pow == null)
      return null;

    return bcmod(env, new StringValueImpl(pow), modulus);
  }


  /**
   * Set the default scale to use for subsequent calls to bcmath functions.
   * The scale is the number of decimal points to include in the string that
   * results from bcmath calculations.
   *
   * A default scale set with this function overrides the value of the
   * "bcmath.scale" ini variable.
   */
  public static boolean bcscale(Env env, int scale)
  {
    env.setIni("bcmath.scale", String.valueOf(scale));

    return true;
  }

  /**
   * Return the square root of an arbitrary precision number.
   *
   * A negative operand results in a warning message and a return value of null.
   *
   * The optional scale indicates the number of decimal digits to include in
   * the result, the default is the value of a previous call to {@link #bcscale}
   * or the value of the ini variable "bcmath.scale".
   */
  public static String bcsqrt(Env env, Value operand, @Optional("-1") int scale)
  {
    scale = calculateScale(env, scale);

    BigDecimal value = toBigDecimal(operand);

    int compareToZero = value.compareTo(ZERO);

    if (compareToZero < 0) {
      env.warning(L.l("square root of negative number"));
      return null;
    }
    else if (compareToZero == 0) {
      return "0";
    }

    int compareToOne = value.compareTo(ONE);

    if (compareToOne == 0)
      return "1";

    // newton's algorithm

    int cscale;

    // initial guess

    BigDecimal initialGuess;

    if (compareToOne < 1) {
      initialGuess = ONE;
      cscale = value.scale();
    }
    else {
      BigInteger integerPart = value.toBigInteger();

      int length = integerPart.toString().length();

      if ((length % 2) == 0)
        length--;

      length /= 2;

      initialGuess = ONE.movePointRight(length);

      cscale = Math.max(scale, value.scale()) + 2;
    }

    // iterate

    BigDecimal guess = initialGuess;

    BigDecimal lastGuess;

    for (int iteration = 0; iteration < SQRT_MAX_ITERATIONS; iteration++) {
      lastGuess = guess;
      guess = value.divide(guess, cscale, RoundingMode.DOWN);
      guess = guess.add(lastGuess);
      guess = guess.divide(TWO, cscale, RoundingMode.DOWN);

      if (lastGuess.equals(guess)) {
          break;
      }
    }

    value = guess;

    value = value.setScale(scale, RoundingMode.DOWN);

    return value.toPlainString();
  }

  /**
   * Subtract arbitrary precision number (value2) from another (value1).
   *
   * The optional scale indicates the number of decimal digits to include in
   * the result, the default is the value of a previous call to {@link #bcscale}
   * or the value of the ini variable "bcmath.scale".
   */
  public static String bcsub(Env env, Value value1, Value value2, @Optional("-1") int scale)
  {
    scale = calculateScale(env, scale);

    BigDecimal bd1 = toBigDecimal(value1);
    BigDecimal bd2 = toBigDecimal(value2);

    BigDecimal bd = bd1.subtract(bd2);

    bd = bd.setScale(scale, RoundingMode.DOWN);

    return bd.toPlainString();
  }
}
