// 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 net.starlark.java.annot.StarlarkBuiltin;

/** The Starlark int data type. */
@StarlarkBuiltin(
    name = "int",
    category = "core",
    doc =
        "The type of integers in Starlark. Starlark integers may be of any magnitude; arithmetic"
            + " is exact. Examples of integer expressions:<br>"
            + "<pre class=\"language-python\">153\n"
            + "0x2A  # hexadecimal literal\n"
            + "0o54  # octal literal\n"
            + "23 * 2 + 5\n"
            + "100 / -7\n"
            + "100 % -7  # -5 (unlike in some other languages)\n"
            + "int(\"18\")\n"
            + "</pre>")
public abstract class StarlarkInt implements StarlarkValue, Comparable<StarlarkInt> {

  // A cache of small integers >= LEAST_SMALLINT.
  private static final int LEAST_SMALLINT = -128;
  private static final Int32[] smallints = new Int32[100_000];

  static final StarlarkInt ZERO = StarlarkInt.of(0);
  private static final StarlarkInt ONE = StarlarkInt.of(1);
  private static final StarlarkInt MINUS_ONE = StarlarkInt.of(-1);

  /** Only nested classes of {@code StarlarkInt} are allowed to inherit it. */
  private StarlarkInt() {}

  /** Returns the Starlark int value that represents x. */
  public static StarlarkInt of(int x) {
    int index = x - LEAST_SMALLINT; // (may overflow)
    if (0 <= index && index < smallints.length) {
      Int32 xi = smallints[index];
      if (xi == null) {
        xi = new Int32(x);
        smallints[index] = xi;
      }
      return xi;
    }
    return new Int32(x);
  }

  /** Returns the Starlark int value that represents x. */
  public static StarlarkInt of(long x) {
    if ((long) (int) x == x) {
      return StarlarkInt.of((int) x);
    }
    return new Int64(x);
  }

  /** Returns the Starlark int value that represents x. */
  public static StarlarkInt of(BigInteger x) {
    if (x.bitLength() < 64) {
      return StarlarkInt.of(x.longValue());
    }
    return new Big(x);
  }

  /**
   * Returns the StarlarkInt value that most closely approximates x.
   *
   * @throws IllegalArgumentException is x is not finite.
   */
  static StarlarkInt ofFiniteDouble(double x) {
    return StarlarkFloat.finiteDoubleToIntExact(x);
  }

  /**
   * Returns the int denoted by a literal string in the specified base, as if by the Starlark
   * expression {@code int(s, base)}.
   *
   * @throws NumberFormatException if the input is invalid.
   */
  public static StarlarkInt parse(String s, int base) {
    String stringForErrors = s;

    if (s.isEmpty()) {
      throw new NumberFormatException("empty string");
    }

    // +/- prefix?
    boolean isNegative = false;
    char c = s.charAt(0);
    if (c == '+') {
      s = s.substring(1);
    } else if (c == '-') {
      s = s.substring(1);
      isNegative = true;
    }

    String digits = s;

    // 0b 0o 0x prefix?
    if (s.length() > 1 && s.charAt(0) == '0') {
      int prefixBase = 0;
      c = s.charAt(1);
      if (c == 'b' || c == 'B') {
        prefixBase = 2;
      } else if (c == 'o' || c == 'O') {
        prefixBase = 8;
      } else if (c == 'x' || c == 'X') {
        prefixBase = 16;
      }
      if (prefixBase != 0) {
        if (base == 0 || base == prefixBase) {
          base = prefixBase;
          digits = s.substring(2); // strip prefix
        }
      }
    }

    // No prefix, no base? Use decimal.
    if (digits == s && base == 0) {
      // Don't infer base when input starts with '0' due to octal/decimal ambiguity.
      if (s.length() > 1 && s.charAt(0) == '0') {
        throw new NumberFormatException(
            "cannot infer base when string begins with a 0: " + Starlark.repr(stringForErrors));
      }
      base = 10;
    }
    if (base < 2 || base > 36) {
      throw new NumberFormatException(
          String.format("invalid base %d (want 2 <= base <= 36)", base));
    }

    // Do not allow Long.parseLong and new BigInteger to accept another +/- sign.
    if (digits.startsWith("+") || digits.startsWith("-")) {
      throw new NumberFormatException(
          String.format("invalid base-%d literal: %s", base, Starlark.repr(stringForErrors)));
    }

    StarlarkInt result;
    try {
      result = StarlarkInt.of(Long.parseLong(digits, base));
    } catch (NumberFormatException unused1) {
      try {
        result = StarlarkInt.of(new BigInteger(digits, base));
      } catch (NumberFormatException unused2) {
        throw new NumberFormatException(
            String.format("invalid base-%d literal: %s", base, Starlark.repr(stringForErrors)));
      }
    }
    return isNegative ? StarlarkInt.uminus(result) : result;
  }

  // Subclass for values exactly representable in a Java int.
  private static final class Int32 extends StarlarkInt {
    final int v;

    Int32(int v) {
      this.v = v;
    }

    @Override
    public int toInt(String what) {
      return v;
    }

    @Override
    public long toLong(String what) {
      return (long) v;
    }

    @Override
    protected long toLongFast() {
      return (long) v;
    }

    @Override
    public BigInteger toBigInteger() {
      return BigInteger.valueOf(v);
    }

    @Override
    public Number toNumber() {
      return v;
    }

    @Override
    public int signum() {
      return Integer.signum(v);
    }

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

    @Override
    public int hashCode() {
      return 0x316c5239 * Integer.hashCode(v) ^ 0x67c4a7d5;
    }

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

  // Subclass for values exactly representable in a Java long.
  private static final class Int64 extends StarlarkInt {
    final long v;

    Int64(long v) {
      this.v = v;
    }

    @Override
    public long toLong(String what) {
      return v;
    }

    @Override
    protected long toLongFast() {
      return v;
    }

    @Override
    public BigInteger toBigInteger() {
      return BigInteger.valueOf(v);
    }

    @Override
    public Number toNumber() {
      return v;
    }

    @Override
    public int signum() {
      return Long.signum(v);
    }

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

    @Override
    public int hashCode() {
      return 0x67c4a7d5 * Long.hashCode(v) ^ 0xee914a1b;
    }

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

  // Subclass for values not exactly representable in a long.
  private static final class Big extends StarlarkInt {
    final BigInteger v;

    Big(BigInteger v) {
      this.v = v;
    }

    @Override
    public BigInteger toBigInteger() {
      return v;
    }

    @Override
    public Number toNumber() {
      return v;
    }

    @Override
    public int signum() {
      return v.signum();
    }

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

    @Override
    public int hashCode() {
      return 0xee914a1b * v.hashCode() ^ 0x6406918f;
    }

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

  /** Returns the value of this StarlarkInt as a Number (Integer, Long, or BigInteger). */
  public abstract Number toNumber();

  /** Returns the signum of this StarlarkInt (-1, 0, or +1). */
  public abstract int signum();

  /** Returns this StarlarkInt as a string of decimal digits. */
  @Override
  public String toString() {
    if (this instanceof Int32) {
      return Integer.toString(((Int32) this).v);
    } else if (this instanceof Int64) {
      return Long.toString(((Int64) this).v);
    } else {
      return toBigInteger().toString();
    }
  }

  @Override
  public abstract void repr(Printer printer);

  /** Returns the signed int32 value of this StarlarkInt, or fails if not exactly representable. */
  public int toInt(String what) throws EvalException {
    throw Starlark.errorf("got %s for %s, want value in signed 32-bit range", this, what);
  }

  /** Returns the signed int64 value of this StarlarkInt, or fails if not exactly representable. */
  public long toLong(String what) throws EvalException {
    throw Starlark.errorf("got %s for %s, want value in the signed 64-bit range", this, what);
  }

  // A preallocated exception used to indicate overflow errors without the cost of allocation.
  private static final Overflow OVERFLOW = new Overflow();

  private static final class Overflow extends Exception {}

  /**
   * Similar to {@link #toLong(String)}, but faster: exception is not allocated and stack trace is
   * not collected.
   */
  protected long toLongFast() throws Overflow {
    throw OVERFLOW;
  }

  /** Returns the nearest IEEE-754 double-precision value closest to this int, which may be ±Inf. */
  public double toDouble() {
    if (this instanceof Int32) {
      return ((Int32) this).v;
    } else if (this instanceof Int64) {
      return ((Int64) this).v;
    } else {
      return toBigInteger().doubleValue(); // may be ±Inf
    }
  }

  /**
   * Returns the nearest IEEE-754 double-precision value closest to this int.
   *
   * @throws EvalException is the int is to large to represent as a finite float value.
   */
  public double toFiniteDouble() throws EvalException {
    double d = toDouble();
    if (!Double.isFinite(d)) {
      throw Starlark.errorf("int too large to convert to float");
    }
    return d;
  }

  /** Returns the BigInteger value of this StarlarkInt. */
  public abstract BigInteger toBigInteger();

  /**
   * Returns the value of this StarlarkInt as a Java signed 32-bit int.
   *
   * @throws IllegalArgumentException if this int is not in that value range.
   */
  public final int toIntUnchecked() throws IllegalArgumentException {
    if (this instanceof Int32) {
      return ((Int32) this).v;
    }
    // Use a constant exception to avoid allocation.
    // This operator is provided for fast access and case discrimination.
    // Use toInt(String) for user-visible errors.
    throw NOT_INT32;
  }

  private static final IllegalArgumentException NOT_INT32 =
      new IllegalArgumentException("not a signed 32-bit value");

  /** Returns the result of truncating this value into the signed 32-bit range. */
  public int truncateToInt() {
    if (this instanceof Int32) {
      return ((Int32) this).v;
    } else if (this instanceof Int64) {
      return (int) ((Int64) this).v;
    } else {
      return toBigInteger().intValue();
    }
  }

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

  @Override
  public boolean truth() {
    return this != ZERO;
  }

  @Override
  public int compareTo(StarlarkInt x) {
    return compare(this, x);
  }

  // binary operators

  /** Returns signum(x - y). */
  public static int compare(StarlarkInt x, StarlarkInt y) {
    // If both arguments are big, we compare BigIntegers.
    // If neither argument is big, we compare longs.
    // If only one argument is big, its magnitude is greater
    // than the other operand, so only its sign matters.
    //
    // We avoid unnecessary branches.
    try {
      long xl = x.toLongFast();
      try {
        long yl = y.toLongFast();
        return Long.compare(xl, yl); // (long, long)
      } catch (Overflow unused) {
        return -((Big) y).v.signum(); // (long, big)
      }
    } catch (Overflow unused) {
      return y instanceof Big
          ? ((Big) x).v.compareTo(((Big) y).v) // (big, big)
          : ((Big) x).v.signum(); // (big, long)
    }
  }

  /** Returns x + y. */
  public static StarlarkInt add(StarlarkInt x, StarlarkInt y) {
    if (x instanceof Int32 && y instanceof Int32) {
      long xl = ((Int32) x).v;
      long yl = ((Int32) y).v;
      return StarlarkInt.of(xl + yl);
    }

    // We avoid Math.addExact and its overheads of exception allocation.
    try {
      long xl = x.toLongFast();
      long yl = y.toLongFast();
      long zl = xl + yl;
      boolean overflow = ((xl ^ zl) & (yl ^ zl)) < 0; // see Hacker's Delight, chapter 2
      if (!overflow) {
        return StarlarkInt.of(zl);
      }
    } catch (Overflow unused) {
      /* fall through */
    }

    BigInteger xbig = x.toBigInteger();
    BigInteger ybig = y.toBigInteger();
    BigInteger zbig = xbig.add(ybig);
    return StarlarkInt.of(zbig);
  }

  /** Returns x - y. */
  public static StarlarkInt subtract(StarlarkInt x, StarlarkInt y) {
    if (x instanceof Int32 && y instanceof Int32) {
      long xl = ((Int32) x).v;
      long yl = ((Int32) y).v;
      return StarlarkInt.of(xl - yl);
    }

    // We avoid Math.subtractExact and its overhead of exception allocation.
    try {
      long xl = x.toLongFast();
      long yl = y.toLongFast();
      long zl = xl - yl;
      boolean overflow = ((xl ^ yl) & (xl ^ zl)) < 0; // see Hacker's Delight, chapter 2
      if (!overflow) {
        return StarlarkInt.of(zl);
      }
    } catch (Overflow unused) {
      /* fall through */
    }

    BigInteger xbig = x.toBigInteger();
    BigInteger ybig = y.toBigInteger();
    BigInteger zbig = xbig.subtract(ybig);
    return StarlarkInt.of(zbig);
  }

  /** Returns x * y. */
  public static StarlarkInt multiply(StarlarkInt x, StarlarkInt y) {
    // Fast path for common case: int32 * int32.
    if (x instanceof Int32 && y instanceof Int32) {
      long xl = ((Int32) x).v;
      long yl = ((Int32) y).v;
      return StarlarkInt.of(xl * yl);
    }

    try {
      long xl = x.toLongFast();
      long yl = y.toLongFast();

      // Signed int128 multiplication, using Hacker's Delight 8-2
      // (High-Order Half of 64-Bit Product) extended to 128 bits.
      // TODO(adonovan): use Math.multiplyHigh when Java 9 becomes available.
      long xlo = xl & 0xFFFFFFFFL;
      long xhi = xl >> 32;
      long ylo = yl & 0xFFFFFFFFL;
      long yhi = yl >> 32;
      long zlo = xlo * ylo;
      long t = xhi * ylo + (zlo >>> 32);
      long z1 = t & 0xFFFFFFFFL;
      long z2 = t >> 32;
      z1 += xlo * yhi;

      // high and low arms of result
      long z128hi = xhi * yhi + z2 + (z1 >> 32);
      long z128lo = xl * yl;

      // Check int128 result is within int64 range.
      if (z128hi == (z128lo & Long.MIN_VALUE) >> 63) {
        return StarlarkInt.of(z128lo);
      }

      /* overflow */

    } catch (Overflow unused) {
      /* fall through */
    }

    // Avoid unnecessary conversion to BigInteger if the other operand is -1, 0, 1.
    // (Also makes self-test below faster.)
    if (x == ZERO || y == ONE) {
      return x;
    } else if (y == ZERO || x == ONE) {
      return y;
    } else if (x == MINUS_ONE) {
      return StarlarkInt.uminus(y);
    } else if (y == MINUS_ONE) {
      return StarlarkInt.uminus(x);
    }

    BigInteger xbig = x.toBigInteger();
    BigInteger ybig = y.toBigInteger();
    BigInteger zbig = xbig.multiply(ybig);
    StarlarkInt z = StarlarkInt.of(zbig);
    // cheap self-test
    if (!(z instanceof Big)) {
      throw new AssertionError(
          String.format(
              "bug in multiplication: %s * %s = %s, must be long multiplication", x, y, z));
    }
    return z;
  }

  /** Returns x // y (floor of integer division). */
  public static StarlarkInt floordiv(StarlarkInt x, StarlarkInt y) throws EvalException {
    if (y == ZERO) {
      throw Starlark.errorf("integer division by zero");
    }
    try {
      long xl = x.toLongFast();
      long yl = y.toLongFast();
      // http://python-history.blogspot.com/2010/08/why-pythons-integer-division-floors.html
      if (xl == Long.MIN_VALUE && yl == -1) {
        /* sole case in which quotient doesn't fit in long */
      } else {
        long quo = Math.floorDiv(xl, yl);
        return StarlarkInt.of(quo);
      }
      /* overflow */
    } catch (Overflow unused) {
      /* fall through */
    }

    BigInteger xbig = x.toBigInteger();
    BigInteger ybig = y.toBigInteger();
    BigInteger[] quorem = xbig.divideAndRemainder(ybig);
    if ((xbig.signum() < 0) != (ybig.signum() < 0) && quorem[1].signum() != 0) {
      quorem[0] = quorem[0].subtract(BigInteger.ONE);
    }
    return StarlarkInt.of(quorem[0]);
  }

  /** Returns x % y. */
  public static StarlarkInt mod(StarlarkInt x, StarlarkInt y) throws EvalException {
    if (y == ZERO) {
      throw Starlark.errorf("integer modulo by zero");
    }
    try {
      long xl = x.toLongFast();
      long yl = y.toLongFast();
      // In Starlark, the sign of the result is the sign of the divisor.
      return StarlarkInt.of(Math.floorMod(xl, yl));
    } catch (Overflow unused) {
      /* fall through */
    }

    BigInteger xbig = x.toBigInteger();
    BigInteger ybig = y.toBigInteger();
    BigInteger zbig = xbig.remainder(ybig);
    if ((x.signum() < 0) != (y.signum() < 0) && zbig.signum() != 0) {
      zbig = zbig.add(ybig);
    }
    return StarlarkInt.of(zbig);
  }

  /** Returns x >> y. */
  public static StarlarkInt shiftRight(StarlarkInt x, StarlarkInt y) throws EvalException {
    int yi = y.toInt("shift count");
    if (yi < 0) {
      throw Starlark.errorf("negative shift count: %d", yi);
    }
    try {
      long xl = x.toLongFast();
      if (yi >= Long.SIZE) {
        return xl < 0 ? StarlarkInt.of(-1) : ZERO;
      }
      return StarlarkInt.of(xl >> yi);
    } catch (Overflow unused) {
      /* fall through */
    }

    BigInteger xbig = x.toBigInteger();
    BigInteger zbig = xbig.shiftRight(yi);
    return StarlarkInt.of(zbig);
  }

  /** Returns x << y. */
  public static StarlarkInt shiftLeft(StarlarkInt x, StarlarkInt y) throws EvalException {
    int yi = y.toInt("shift count");
    if (yi < 0) {
      throw Starlark.errorf("negative shift count: %d", yi);
    } else if (yi >= 512) {
      throw Starlark.errorf("shift count too large: %d", yi);
    }
    try {
      long xl = x.toLongFast();
      long z = xl << yi; // only uses low 6 bits of yi
      if ((z >> yi) == xl && yi < 64) {
        return StarlarkInt.of(z);
      }
      /* overflow */
    } catch (Overflow unused) {
      /* fall through */
    }

    BigInteger xbig = x.toBigInteger();
    BigInteger zbig = xbig.shiftLeft(yi);
    return StarlarkInt.of(zbig);
  }

  /** Returns x ^ y. */
  public static StarlarkInt xor(StarlarkInt x, StarlarkInt y) {
    try {
      long xl = x.toLongFast();
      long yl = y.toLongFast();
      return StarlarkInt.of(xl ^ yl);
    } catch (Overflow unused) {
      /* fall through */
    }

    BigInteger xbig = x.toBigInteger();
    BigInteger ybig = y.toBigInteger();
    BigInteger zbig = xbig.xor(ybig);
    return StarlarkInt.of(zbig);
  }

  /** Returns x | y. */
  public static StarlarkInt or(StarlarkInt x, StarlarkInt y) {
    try {
      long xl = x.toLongFast();
      long yl = y.toLongFast();
      return StarlarkInt.of(xl | yl);
    } catch (Overflow unused) {
      /* fall through */
    }

    BigInteger xbig = x.toBigInteger();
    BigInteger ybig = y.toBigInteger();
    BigInteger zbig = xbig.or(ybig);
    return StarlarkInt.of(zbig);
  }

  /** Returns x & y. */
  public static StarlarkInt and(StarlarkInt x, StarlarkInt y) {
    try {
      long xl = x.toLongFast();
      long yl = y.toLongFast();
      return StarlarkInt.of(xl & yl);
    } catch (Overflow unused) {
      /* fall through */
    }

    BigInteger xbig = x.toBigInteger();
    BigInteger ybig = y.toBigInteger();
    BigInteger zbig = xbig.and(ybig);
    return StarlarkInt.of(zbig);
  }

  /** Returns ~x. */
  public static StarlarkInt bitnot(StarlarkInt x) {
    try {
      long xl = x.toLongFast();
      return StarlarkInt.of(~xl);
    } catch (Overflow unused) {
      /* fall through */
    }

    BigInteger xbig = ((Big) x).v;
    return StarlarkInt.of(xbig.not());
  }

  /** Returns -x. */
  public static StarlarkInt uminus(StarlarkInt x) {
    if (x instanceof Int32) {
      long xl = ((Int32) x).v;
      return StarlarkInt.of(-xl);
    }

    if (x instanceof Int64) {
      long xl = ((Int64) x).v;
      if (xl != Long.MIN_VALUE) {
        return StarlarkInt.of(-xl);
      }
    }

    BigInteger xbig = x.toBigInteger();
    BigInteger ybig = xbig.negate();
    return StarlarkInt.of(ybig);
  }

  /** Reports whether int x exactly equals float y. */
  static boolean intEqualsFloat(StarlarkInt x, StarlarkFloat y) {
    double yf = y.toDouble();
    return !Double.isNaN(yf) && compareIntAndDouble(x, yf) == 0;
  }

  /** Returns an exact three-valued comparison of int x with (non-NaN) double y. */
  static int compareIntAndDouble(StarlarkInt x, double y) {
    if (Double.isInfinite(y)) {
      return y > 0 ? -1 : +1;
    }

    // For Int32 and some Int64s, the toDouble conversion is exact.
    if (x instanceof StarlarkInt.Int32
        || (x instanceof StarlarkInt.Int64 && longHasExactDouble(((Int64) x).v))) {
      // Avoid Double.compare: it believes -0.0 < 0.0.
      double xf = x.toDouble();
      if (xf > y) {
        return +1;
      } else if (xf < y) {
        return -1;
      }
      return 0;
    }

    // If signs differ, we needn't look at magnitude.
    int xsign = x.signum();
    int ysign = (int) Math.signum(y);
    if (xsign > ysign) {
      return +1;
    } else if (xsign < ysign) {
      return -1;
    }

    // Left-shift either the int or the float mantissa,
    // then compare the resulting integers.
    int shift = StarlarkFloat.getShift(y);
    BigInteger xbig = x.toBigInteger();
    if (shift < 0) {
      xbig = xbig.shiftLeft(-shift);
    }
    BigInteger ybig = BigInteger.valueOf(StarlarkFloat.getMantissa(y));
    if (shift > 0) {
      ybig = ybig.shiftLeft(shift);
    }
    return xbig.compareTo(ybig);
  }

  private static boolean longHasExactDouble(long x) {
    return (long) (double) x == x;
  }
}
