/*
 * Licensed to the Apache Software Foundation (ASF) under one or more
 * contributor license agreements.  See the NOTICE file distributed with
 * this work for additional information regarding copyright ownership.
 * The ASF licenses this file to You 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 org.apache.lucene.util;

import java.math.BigInteger;
import java.util.Arrays;
import org.apache.lucene.tests.util.LuceneTestCase;
import org.apache.lucene.tests.util.TestUtil;

/** Tests for NumericUtils static methods. */
public class TestNumericUtils extends LuceneTestCase {

  /**
   * generate a series of encoded longs, each numerical one bigger than the one before. check for
   * correct ordering of the encoded bytes and that values round-trip.
   */
  public void testLongConversionAndOrdering() throws Exception {
    BytesRef previous = null;
    BytesRef current = new BytesRef(new byte[Long.BYTES]);
    for (long value = -100000L; value < 100000L; value++) {
      NumericUtils.longToSortableBytes(value, current.bytes, current.offset);
      if (previous == null) {
        previous = new BytesRef(new byte[Long.BYTES]);
      } else {
        // test if smaller
        assertTrue("current bigger than previous: ", previous.compareTo(current) < 0);
      }
      // test is back and forward conversion works
      assertEquals(
          "forward and back conversion should generate same long",
          value,
          NumericUtils.sortableBytesToLong(current.bytes, current.offset));
      // next step
      System.arraycopy(
          current.bytes, current.offset, previous.bytes, previous.offset, current.length);
    }
  }

  /**
   * generate a series of encoded ints, each numerical one bigger than the one before. check for
   * correct ordering of the encoded bytes and that values round-trip.
   */
  public void testIntConversionAndOrdering() throws Exception {
    BytesRef previous = null;
    BytesRef current = new BytesRef(new byte[Integer.BYTES]);
    for (int value = -100000; value < 100000; value++) {
      NumericUtils.intToSortableBytes(value, current.bytes, current.offset);
      if (previous == null) {
        previous = new BytesRef(new byte[Integer.BYTES]);
      } else {
        // test if smaller
        assertTrue("current bigger than previous: ", previous.compareTo(current) < 0);
      }
      // test is back and forward conversion works
      assertEquals(
          "forward and back conversion should generate same int",
          value,
          NumericUtils.sortableBytesToInt(current.bytes, current.offset));
      // next step
      System.arraycopy(
          current.bytes, current.offset, previous.bytes, previous.offset, current.length);
    }
  }

  /**
   * generate a series of encoded BigIntegers, each numerical one bigger than the one before. check
   * for correct ordering of the encoded bytes and that values round-trip.
   */
  public void testBigIntConversionAndOrdering() throws Exception {
    // we need at least 3 bytes of storage.
    int size = TestUtil.nextInt(random(), 3, 16);
    BytesRef previous = null;
    BytesRef current = new BytesRef(new byte[size]);
    for (long value = -100000L; value < 100000L; value++) {
      NumericUtils.bigIntToSortableBytes(
          BigInteger.valueOf(value), size, current.bytes, current.offset);
      if (previous == null) {
        previous = new BytesRef(new byte[size]);
      } else {
        // test if smaller
        assertTrue("current bigger than previous: ", previous.compareTo(current) < 0);
      }
      // test is back and forward conversion works
      assertEquals(
          "forward and back conversion should generate same BigInteger",
          BigInteger.valueOf(value),
          NumericUtils.sortableBytesToBigInt(current.bytes, current.offset, current.length));
      // next step
      System.arraycopy(
          current.bytes, current.offset, previous.bytes, previous.offset, current.length);
    }
  }

  /**
   * check extreme values of longs check for correct ordering of the encoded bytes and that values
   * round-trip.
   */
  public void testLongSpecialValues() throws Exception {
    long[] values =
        new long[] {
          Long.MIN_VALUE,
          Long.MIN_VALUE + 1,
          Long.MIN_VALUE + 2,
          -5003400000000L,
          -4000L,
          -3000L,
          -2000L,
          -1000L,
          -1L,
          0L,
          1L,
          10L,
          300L,
          50006789999999999L,
          Long.MAX_VALUE - 2,
          Long.MAX_VALUE - 1,
          Long.MAX_VALUE
        };
    BytesRef[] encoded = new BytesRef[values.length];

    for (int i = 0; i < values.length; i++) {
      encoded[i] = new BytesRef(new byte[Long.BYTES]);
      NumericUtils.longToSortableBytes(values[i], encoded[i].bytes, encoded[i].offset);

      // check forward and back conversion
      assertEquals(
          "forward and back conversion should generate same long",
          values[i],
          NumericUtils.sortableBytesToLong(encoded[i].bytes, encoded[i].offset));
    }

    // check sort order (encoded values should be ascending)
    for (int i = 1; i < encoded.length; i++) {
      assertTrue("check sort order", encoded[i - 1].compareTo(encoded[i]) < 0);
    }
  }

  /**
   * check extreme values of ints check for correct ordering of the encoded bytes and that values
   * round-trip.
   */
  public void testIntSpecialValues() throws Exception {
    int[] values =
        new int[] {
          Integer.MIN_VALUE,
          Integer.MIN_VALUE + 1,
          Integer.MIN_VALUE + 2,
          -64765767,
          -4000,
          -3000,
          -2000,
          -1000,
          -1,
          0,
          1,
          10,
          300,
          765878989,
          Integer.MAX_VALUE - 2,
          Integer.MAX_VALUE - 1,
          Integer.MAX_VALUE
        };
    BytesRef[] encoded = new BytesRef[values.length];

    for (int i = 0; i < values.length; i++) {
      encoded[i] = new BytesRef(new byte[Integer.BYTES]);
      NumericUtils.intToSortableBytes(values[i], encoded[i].bytes, encoded[i].offset);

      // check forward and back conversion
      assertEquals(
          "forward and back conversion should generate same int",
          values[i],
          NumericUtils.sortableBytesToInt(encoded[i].bytes, encoded[i].offset));
    }

    // check sort order (encoded values should be ascending)
    for (int i = 1; i < encoded.length; i++) {
      assertTrue("check sort order", encoded[i - 1].compareTo(encoded[i]) < 0);
    }
  }

  /**
   * check extreme values of big integers (4 bytes) check for correct ordering of the encoded bytes
   * and that values round-trip.
   */
  public void testBigIntSpecialValues() throws Exception {
    BigInteger[] values =
        new BigInteger[] {
          BigInteger.valueOf(Integer.MIN_VALUE),
          BigInteger.valueOf(Integer.MIN_VALUE + 1),
          BigInteger.valueOf(Integer.MIN_VALUE + 2),
          BigInteger.valueOf(-64765767),
          BigInteger.valueOf(-4000),
          BigInteger.valueOf(-3000),
          BigInteger.valueOf(-2000),
          BigInteger.valueOf(-1000),
          BigInteger.valueOf(-1),
          BigInteger.valueOf(0),
          BigInteger.valueOf(1),
          BigInteger.valueOf(10),
          BigInteger.valueOf(300),
          BigInteger.valueOf(765878989),
          BigInteger.valueOf(Integer.MAX_VALUE - 2),
          BigInteger.valueOf(Integer.MAX_VALUE - 1),
          BigInteger.valueOf(Integer.MAX_VALUE)
        };
    BytesRef[] encoded = new BytesRef[values.length];

    for (int i = 0; i < values.length; i++) {
      encoded[i] = new BytesRef(new byte[Integer.BYTES]);
      NumericUtils.bigIntToSortableBytes(
          values[i], Integer.BYTES, encoded[i].bytes, encoded[i].offset);

      // check forward and back conversion
      assertEquals(
          "forward and back conversion should generate same big integer",
          values[i],
          NumericUtils.sortableBytesToBigInt(encoded[i].bytes, encoded[i].offset, Integer.BYTES));
    }

    // check sort order (encoded values should be ascending)
    for (int i = 1; i < encoded.length; i++) {
      assertTrue("check sort order", encoded[i - 1].compareTo(encoded[i]) < 0);
    }
  }

  /**
   * check various sorted values of doubles (including extreme values) check for correct ordering of
   * the encoded bytes and that values round-trip.
   */
  public void testDoubles() throws Exception {
    double[] values =
        new double[] {
          Double.NEGATIVE_INFINITY,
          -2.3E25,
          -1.0E15,
          -1.0,
          -1.0E-1,
          -1.0E-2,
          -0.0,
          +0.0,
          1.0E-2,
          1.0E-1,
          1.0,
          1.0E15,
          2.3E25,
          Double.POSITIVE_INFINITY,
          Double.NaN
        };
    long[] encoded = new long[values.length];

    // check forward and back conversion
    for (int i = 0; i < values.length; i++) {
      encoded[i] = NumericUtils.doubleToSortableLong(values[i]);
      assertTrue(
          "forward and back conversion should generate same double",
          Double.compare(values[i], NumericUtils.sortableLongToDouble(encoded[i])) == 0);
    }

    // check sort order (encoded values should be ascending)
    for (int i = 1; i < encoded.length; i++) {
      assertTrue("check sort order", encoded[i - 1] < encoded[i]);
    }
  }

  public static final double[] DOUBLE_NANs = {
    Double.NaN,
    Double.longBitsToDouble(0x7ff0000000000001L),
    Double.longBitsToDouble(0x7fffffffffffffffL),
    Double.longBitsToDouble(0xfff0000000000001L),
    Double.longBitsToDouble(0xffffffffffffffffL)
  };

  public void testSortableDoubleNaN() {
    final long plusInf = NumericUtils.doubleToSortableLong(Double.POSITIVE_INFINITY);
    for (double nan : DOUBLE_NANs) {
      assertTrue(Double.isNaN(nan));
      final long sortable = NumericUtils.doubleToSortableLong(nan);
      assertTrue(
          "Double not sorted correctly: "
              + nan
              + ", long repr: "
              + sortable
              + ", positive inf.: "
              + plusInf,
          sortable > plusInf);
    }
  }

  /**
   * check various sorted values of floats (including extreme values) check for correct ordering of
   * the encoded bytes and that values round-trip.
   */
  public void testFloats() throws Exception {
    float[] values =
        new float[] {
          Float.NEGATIVE_INFINITY,
          -2.3E25f,
          -1.0E15f,
          -1.0f,
          -1.0E-1f,
          -1.0E-2f,
          -0.0f,
          +0.0f,
          1.0E-2f,
          1.0E-1f,
          1.0f,
          1.0E15f,
          2.3E25f,
          Float.POSITIVE_INFINITY,
          Float.NaN
        };
    int[] encoded = new int[values.length];

    // check forward and back conversion
    for (int i = 0; i < values.length; i++) {
      encoded[i] = NumericUtils.floatToSortableInt(values[i]);
      assertTrue(
          "forward and back conversion should generate same float",
          Float.compare(values[i], NumericUtils.sortableIntToFloat(encoded[i])) == 0);
    }

    // check sort order (encoded values should be ascending)
    for (int i = 1; i < encoded.length; i++) {
      assertTrue("check sort order", encoded[i - 1] < encoded[i]);
    }
  }

  public static final float[] FLOAT_NANs = {
    Float.NaN,
    Float.intBitsToFloat(0x7f800001),
    Float.intBitsToFloat(0x7fffffff),
    Float.intBitsToFloat(0xff800001),
    Float.intBitsToFloat(0xffffffff)
  };

  public void testSortableFloatNaN() {
    final int plusInf = NumericUtils.floatToSortableInt(Float.POSITIVE_INFINITY);
    for (float nan : FLOAT_NANs) {
      assertTrue(Float.isNaN(nan));
      final int sortable = NumericUtils.floatToSortableInt(nan);
      assertTrue(
          "Float not sorted correctly: "
              + nan
              + ", int repr: "
              + sortable
              + ", positive inf.: "
              + plusInf,
          sortable > plusInf);
    }
  }

  public void testAdd() throws Exception {
    int iters = atLeast(1000);
    int numBytes = TestUtil.nextInt(random(), 1, 100);
    for (int iter = 0; iter < iters; iter++) {
      BigInteger v1 = new BigInteger(8 * numBytes - 1, random());
      BigInteger v2 = new BigInteger(8 * numBytes - 1, random());

      byte[] v1Bytes = new byte[numBytes];
      byte[] v1RawBytes = v1.toByteArray();
      assert v1RawBytes.length <= numBytes;
      System.arraycopy(
          v1RawBytes, 0, v1Bytes, v1Bytes.length - v1RawBytes.length, v1RawBytes.length);

      byte[] v2Bytes = new byte[numBytes];
      byte[] v2RawBytes = v2.toByteArray();
      assert v1RawBytes.length <= numBytes;
      System.arraycopy(
          v2RawBytes, 0, v2Bytes, v2Bytes.length - v2RawBytes.length, v2RawBytes.length);

      byte[] result = new byte[numBytes];
      NumericUtils.add(numBytes, 0, v1Bytes, v2Bytes, result);

      BigInteger sum = v1.add(v2);
      assertTrue(
          "sum=" + sum + " v1=" + v1 + " v2=" + v2 + " but result=" + new BigInteger(1, result),
          sum.equals(new BigInteger(1, result)));
    }
  }

  public void testIllegalAdd() throws Exception {
    byte[] bytes = new byte[4];
    Arrays.fill(bytes, (byte) 0xff);
    byte[] one = new byte[4];
    one[3] = 1;
    IllegalArgumentException expected =
        expectThrows(
            IllegalArgumentException.class,
            () -> {
              NumericUtils.add(4, 0, bytes, one, new byte[4]);
            });
    assertEquals("a + b overflows bytesPerDim=4", expected.getMessage());
  }

  public void testSubtract() throws Exception {
    int iters = atLeast(1000);
    int numBytes = TestUtil.nextInt(random(), 1, 100);
    for (int iter = 0; iter < iters; iter++) {
      BigInteger v1 = new BigInteger(8 * numBytes - 1, random());
      BigInteger v2 = new BigInteger(8 * numBytes - 1, random());
      if (v1.compareTo(v2) < 0) {
        BigInteger tmp = v1;
        v1 = v2;
        v2 = tmp;
      }

      byte[] v1Bytes = new byte[numBytes];
      byte[] v1RawBytes = v1.toByteArray();
      assert v1RawBytes.length <= numBytes
          : "length=" + v1RawBytes.length + " vs numBytes=" + numBytes;
      System.arraycopy(
          v1RawBytes, 0, v1Bytes, v1Bytes.length - v1RawBytes.length, v1RawBytes.length);

      byte[] v2Bytes = new byte[numBytes];
      byte[] v2RawBytes = v2.toByteArray();
      assert v2RawBytes.length <= numBytes;
      assert v2RawBytes.length <= numBytes
          : "length=" + v2RawBytes.length + " vs numBytes=" + numBytes;
      System.arraycopy(
          v2RawBytes, 0, v2Bytes, v2Bytes.length - v2RawBytes.length, v2RawBytes.length);

      byte[] result = new byte[numBytes];
      NumericUtils.subtract(numBytes, 0, v1Bytes, v2Bytes, result);

      BigInteger diff = v1.subtract(v2);

      assertTrue(
          "diff=" + diff + " vs result=" + new BigInteger(result) + " v1=" + v1 + " v2=" + v2,
          diff.equals(new BigInteger(result)));
    }
  }

  public void testIllegalSubtract() throws Exception {
    byte[] v1 = new byte[4];
    v1[3] = (byte) 0xf0;
    byte[] v2 = new byte[4];
    v2[3] = (byte) 0xf1;
    IllegalArgumentException expected =
        expectThrows(
            IllegalArgumentException.class,
            () -> {
              NumericUtils.subtract(4, 0, v1, v2, new byte[4]);
            });
    assertEquals("a < b", expected.getMessage());
  }

  /** test round-trip encoding of random integers */
  public void testIntsRoundTrip() {
    byte[] encoded = new byte[Integer.BYTES];

    for (int i = 0; i < 10000; i++) {
      int value = random().nextInt();
      NumericUtils.intToSortableBytes(value, encoded, 0);
      assertEquals(value, NumericUtils.sortableBytesToInt(encoded, 0));
    }
  }

  /** test round-trip encoding of random longs */
  public void testLongsRoundTrip() {
    byte[] encoded = new byte[Long.BYTES];

    for (int i = 0; i < 10000; i++) {
      long value = TestUtil.nextLong(random(), Long.MIN_VALUE, Long.MAX_VALUE);
      NumericUtils.longToSortableBytes(value, encoded, 0);
      assertEquals(value, NumericUtils.sortableBytesToLong(encoded, 0));
    }
  }

  /** test round-trip encoding of random floats */
  public void testFloatsRoundTrip() {
    byte[] encoded = new byte[Float.BYTES];

    for (int i = 0; i < 10000; i++) {
      float value = Float.intBitsToFloat(random().nextInt());
      NumericUtils.intToSortableBytes(NumericUtils.floatToSortableInt(value), encoded, 0);
      float actual = NumericUtils.sortableIntToFloat(NumericUtils.sortableBytesToInt(encoded, 0));
      assertEquals(Float.floatToIntBits(value), Float.floatToIntBits(actual));
    }
  }

  /** test round-trip encoding of random doubles */
  public void testDoublesRoundTrip() {
    byte[] encoded = new byte[Double.BYTES];

    for (int i = 0; i < 10000; i++) {
      double value =
          Double.longBitsToDouble(TestUtil.nextLong(random(), Long.MIN_VALUE, Long.MAX_VALUE));
      NumericUtils.longToSortableBytes(NumericUtils.doubleToSortableLong(value), encoded, 0);
      double actual =
          NumericUtils.sortableLongToDouble(NumericUtils.sortableBytesToLong(encoded, 0));
      assertEquals(Double.doubleToLongBits(value), Double.doubleToLongBits(actual));
    }
  }

  /** test round-trip encoding of random big integers */
  public void testBigIntsRoundTrip() {
    for (int i = 0; i < 10000; i++) {
      BigInteger value = TestUtil.nextBigInteger(random(), 16);
      int length = value.toByteArray().length;

      // make sure sign extension is tested: sometimes pad to more bytes when encoding.
      int maxLength = TestUtil.nextInt(random(), length, length + 3);
      byte[] encoded = new byte[maxLength];
      NumericUtils.bigIntToSortableBytes(value, maxLength, encoded, 0);
      assertEquals(value, NumericUtils.sortableBytesToBigInt(encoded, 0, maxLength));
    }
  }

  /** check sort order of random integers consistent with Integer.compare */
  public void testIntsCompare() {
    BytesRef left = new BytesRef(new byte[Integer.BYTES]);
    BytesRef right = new BytesRef(new byte[Integer.BYTES]);

    for (int i = 0; i < 10000; i++) {
      int leftValue = random().nextInt();
      NumericUtils.intToSortableBytes(leftValue, left.bytes, left.offset);

      int rightValue = random().nextInt();
      NumericUtils.intToSortableBytes(rightValue, right.bytes, right.offset);

      assertEquals(
          Integer.signum(Integer.compare(leftValue, rightValue)),
          Integer.signum(left.compareTo(right)));
    }
  }

  /** check sort order of random longs consistent with Long.compare */
  public void testLongsCompare() {
    BytesRef left = new BytesRef(new byte[Long.BYTES]);
    BytesRef right = new BytesRef(new byte[Long.BYTES]);

    for (int i = 0; i < 10000; i++) {
      long leftValue = TestUtil.nextLong(random(), Long.MIN_VALUE, Long.MAX_VALUE);
      NumericUtils.longToSortableBytes(leftValue, left.bytes, left.offset);

      long rightValue = TestUtil.nextLong(random(), Long.MIN_VALUE, Long.MAX_VALUE);
      NumericUtils.longToSortableBytes(rightValue, right.bytes, right.offset);

      assertEquals(
          Integer.signum(Long.compare(leftValue, rightValue)),
          Integer.signum(left.compareTo(right)));
    }
  }

  /** check sort order of random floats consistent with Float.compare */
  public void testFloatsCompare() {
    BytesRef left = new BytesRef(new byte[Float.BYTES]);
    BytesRef right = new BytesRef(new byte[Float.BYTES]);

    for (int i = 0; i < 10000; i++) {
      float leftValue = Float.intBitsToFloat(random().nextInt());
      NumericUtils.intToSortableBytes(
          NumericUtils.floatToSortableInt(leftValue), left.bytes, left.offset);

      float rightValue = Float.intBitsToFloat(random().nextInt());
      NumericUtils.intToSortableBytes(
          NumericUtils.floatToSortableInt(rightValue), right.bytes, right.offset);

      assertEquals(
          Integer.signum(Float.compare(leftValue, rightValue)),
          Integer.signum(left.compareTo(right)));
    }
  }

  /** check sort order of random doubles consistent with Double.compare */
  public void testDoublesCompare() {
    BytesRef left = new BytesRef(new byte[Double.BYTES]);
    BytesRef right = new BytesRef(new byte[Double.BYTES]);

    for (int i = 0; i < 10000; i++) {
      double leftValue =
          Double.longBitsToDouble(TestUtil.nextLong(random(), Long.MIN_VALUE, Long.MAX_VALUE));
      NumericUtils.longToSortableBytes(
          NumericUtils.doubleToSortableLong(leftValue), left.bytes, left.offset);

      double rightValue =
          Double.longBitsToDouble(TestUtil.nextLong(random(), Long.MIN_VALUE, Long.MAX_VALUE));
      NumericUtils.longToSortableBytes(
          NumericUtils.doubleToSortableLong(rightValue), right.bytes, right.offset);

      assertEquals(
          Integer.signum(Double.compare(leftValue, rightValue)),
          Integer.signum(left.compareTo(right)));
    }
  }

  /** check sort order of random bigintegers consistent with BigInteger.compareTo */
  public void testBigIntsCompare() {
    for (int i = 0; i < 10000; i++) {
      int maxLength = TestUtil.nextInt(random(), 1, 16);

      BigInteger leftValue = TestUtil.nextBigInteger(random(), maxLength);
      BytesRef left = new BytesRef(new byte[maxLength]);
      NumericUtils.bigIntToSortableBytes(leftValue, maxLength, left.bytes, left.offset);

      BigInteger rightValue = TestUtil.nextBigInteger(random(), maxLength);
      BytesRef right = new BytesRef(new byte[maxLength]);
      NumericUtils.bigIntToSortableBytes(rightValue, maxLength, right.bytes, right.offset);

      assertEquals(
          Integer.signum(leftValue.compareTo(rightValue)), Integer.signum(left.compareTo(right)));
    }
  }
}
