// Copyright 2020 The Bazel Authors. All rights reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
//    http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

package net.starlark.java.eval;

import java.math.BigInteger;
import java.util.Locale;
import net.starlark.java.annot.StarlarkBuiltin;

/** The Starlark float data type. */
@StarlarkBuiltin(
    name = "float",
    category = "core",
    doc = "The type of floating-point numbers in Starlark.")
public final class StarlarkFloat implements StarlarkValue, Comparable<StarlarkFloat> {

  private final double v;

  private StarlarkFloat(double v) {
    this.v = v;
  }

  /** Returns the Starlark float value that represents x. */
  public static StarlarkFloat of(double v) {
    return new StarlarkFloat(v);
  }

  /** Returns the value of this float. */
  public double toDouble() {
    return v;
  }

  @Override
  public String toString() {
    return format(v, 'g');
  }

  @Override
  public void repr(Printer printer) {
    printer.append(toString());
  }

  @Override
  public boolean isImmutable() {
    return true;
  }

  @Override
  public boolean truth() {
    return this.v != 0.0;
  }

  /**
   * Defines a total order over float values. Positive and negative zero values compare equal. NaN
   * compares equal to itself and greater than +Inf.
   */
  @Override
  public int compareTo(StarlarkFloat that) {
    double x = this.v;
    double y = that.v;
    if (x > y) {
      return +1;
    } else if (x < y) {
      return -1;
    } else if (x == y) {
      return 0; // 0.0 == -0.0
    }

    // At least one operand is NaN.
    // Canonicalize NaNs using doubleToLongBits and compare bits.
    long xbits = Double.doubleToLongBits(x);
    long ybits = Double.doubleToLongBits(y);
    return Long.compare(xbits, ybits); // NaN > non-NaN
  }

  @Override
  public int hashCode() {
    // Equal float and int values must yield the same hash.
    if (Double.isFinite(v) && v == Math.rint(v)) {
      return StarlarkInt.ofFiniteDouble(v).hashCode();
    }

    // For non-integral values we can use a cheaper hash.
    // Hashing the bits is consistent with equals
    // because v is neither 0.0 nor -0.0.
    long bits = Double.doubleToLongBits(v); // canonicalizes NaNs
    return (int) (bits ^ (bits >>> 32));
  }

  @Override
  public boolean equals(Object that) {
    return (that instanceof StarlarkFloat && equal(this.v, ((StarlarkFloat) that).v))
        || (that instanceof StarlarkInt && StarlarkInt.intEqualsFloat((StarlarkInt) that, this));
  }

  // equal is an equivalence relation consistent with hashCode and compareTo.
  private static boolean equal(double x, double y) {
    return x == y || (Double.isNaN(x) && Double.isNaN(y));
  }

  // Performs printf-style string conversion of a double value v.
  // conv is one of [efgEFG].
  static String format(double v, char conv) {
    if (!Double.isFinite(v)) {
      if (v == Double.POSITIVE_INFINITY) {
        return "+inf";
      } else if (v == Double.NEGATIVE_INFINITY) {
        return "-inf";
      } else {
        return "nan";
      }
    }

    String s;
    switch (conv) {
      case 'e':
        s = String.format(Locale.US, "%e", v);
        break;
      case 'E':
        s = String.format(Locale.US, "%E", v);
        break;
      case 'f':
      case 'F': // an alias
        s = String.format(Locale.US, "%f", v);
        break;
      case 'g':
        s = String.format(Locale.US, "%.17g", v); // use DBL_DECIMAL_DIG places
        break;
      case 'G':
        s = String.format(Locale.US, "%.17G", v);
        break;
      default:
        throw new IllegalArgumentException("unsupported conversion: " + conv);
    }

    // %g is the default format used by str.
    // It always includes a '.' or an 'e', to make clear that
    // the value is a float, not an int.
    //
    // TODO(adonovan): round the value to the minimal precision required
    // to avoid ambiguity. This requires computing the decimal digit
    // strings of the adjacent floating-point values and then taking the
    // shortest prefix sufficient to distinguish v from them, or using a
    // more sophisticated algorithm such as Florian Loitsch's Grisu3 or
    // Ulf Adams' Ryu.  (Is there an easy way to compute the required
    // precision without materializing the digits? If so we could delegate
    // to format("%*g", prec, v).)
    //
    // For now, we just clean up the output of Java's %.17g implementation,
    // which is unambiguous, but may yield unnecessarily long digit strings
    // such as 1000000000000.0.
    if (conv == 'g' || conv == 'G') {
      int e = s.indexOf(conv == 'g' ? 'e' : 'E');
      if (e < 0) {
        int dot = s.indexOf('.');
        if (dot < 0) {
          // Ensure result always has a decimal point if no exponent.
          // "123" -> "123.0"
          s += ".0";
        } else {
          // Remove trailing zeros after decimal point.
          // "1.110" => "1.11"
          // "1.000" => "1.0"
          int i;
          for (i = s.length() - 1; i > dot + 1 && s.charAt(i) == '0'; i--) {}
          s = s.substring(0, i + 1);
        }
      } else {
        // Remove trailing zeros from mantissa.
        // "1.23000e+45" => "1.23e+45"
        // "1.00000e+45" => "1e+45"
        int i;
        for (i = e - 1; s.charAt(i) == '0'; i--) {}
        if (s.charAt(i) == '.') {
          i--;
        }
        if (i < e - 1) {
          s =
              new StringBuilder(i + 1 + s.length() - e)
                  .append(s, 0, i + 1) // "1.23"
                  .append(s, e, s.length()) // "e+45"
                  .toString();
        }
      }
    }

    return s;
  }

  /** Returns x // y (floor of division). */
  static StarlarkFloat floordiv(double x, double y) throws EvalException {
    if (y == 0.0) {
      throw Starlark.errorf("integer division by zero");
    }
    return StarlarkFloat.of(Math.floor(x / y));
  }

  /** Returns x / y (floating-point division). */
  static StarlarkFloat div(double x, double y) throws EvalException {
    if (y == 0.0) {
      throw Starlark.errorf("floating-point division by zero");
    }
    return StarlarkFloat.of(x / y);
  }

  /** Returns x % y (floating-point remainder). */
  static StarlarkFloat mod(double x, double y) throws EvalException {
    if (y == 0.0) {
      throw Starlark.errorf("floating-point modulo by zero");
    }
    // In Starlark, the sign of the result is the sign of the divisor.
    double z = x % y;
    if ((x < 0) != (y < 0) && z != 0) {
      z += y;
    }
    return StarlarkFloat.of(z);
  }

  /**
   * Returns the Starlark int value closest to x, truncating towards zero.
   *
   * @throws IllegalArgumentException if x is not finite (NaN or ±Inf).
   */
  static StarlarkInt finiteDoubleToIntExact(double x) {
    // small value?
    if (Long.MIN_VALUE <= x && x <= Long.MAX_VALUE) {
      return StarlarkInt.of((long) x);
    }

    // Shift must be positive, because we just handled all small values.
    int shift = getShift(x);
    if (shift <= 0) {
      throw new IllegalStateException("non-positive shift");
    }

    // Shift mantissa by exponent.
    long mantissa = getMantissa(x);
    return StarlarkInt.of(BigInteger.valueOf(mantissa).shiftLeft(shift));
  }

  private static final int EXPONENT_MASK = (1 << 11) - 1;

  // Returns the effective signed mantissa of x.
  // Precondition: x is finite.
  static long getMantissa(double x) {
    long bits = Double.doubleToRawLongBits(x);
    long mantissa = bits & ((1L << 52) - 1);
    int exp = ((int) (bits >>> 52)) & EXPONENT_MASK;
    switch (exp) {
      case 0: // denormal
        break;
      case EXPONENT_MASK:
        throw new IllegalArgumentException("not finite: " + x);
      default: // normal
        mantissa |= 1L << 52;
        break;
    }
    return x < 0 ? -mantissa : mantissa;
  }

  // Returns the effective left (+) or right (-) shift required of the value returned by
  // getMantissa(x).
  // Precondition: x is finite.
  static int getShift(double x) {
    long bits = Double.doubleToRawLongBits(x);
    int exp = ((int) (bits >>> 52)) & EXPONENT_MASK;
    switch (exp) {
      case 0: // denormal
        exp -= 1022;
        break;
      case EXPONENT_MASK:
        throw new IllegalArgumentException("not finite: " + x);
      default: // normal
        exp -= 1023;
        break;
    }
    return exp - 52;
  }
}
