/*
 * 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.hadoop.hive.common.type;

import static org.junit.Assert.*;

import java.math.BigInteger;

import org.junit.After;
import org.junit.Before;
import org.junit.Test;

/**
 * Testcases for {@link UnsignedInt128}
 *
 * This code was based on code from Microsoft's PolyBase.
 */
public class TestUnsignedInt128 {
  private UnsignedInt128 zero;

  private UnsignedInt128 one;

  private UnsignedInt128 two;

  @Before
  public void setUp() throws Exception {
    zero = new UnsignedInt128(0);
    one = new UnsignedInt128(1);
    two = new UnsignedInt128(2);
  }

  @After
  public void tearDown() throws Exception {
  }

  @Test
  public void testHashCode() {
    assertNotEquals(one.hashCode(), two.hashCode());
    assertNotEquals(zero.hashCode(), one.hashCode());
    assertNotEquals(zero.hashCode(), two.hashCode());

    assertEquals(zero.hashCode(), new UnsignedInt128(0).hashCode());
    assertEquals(one.hashCode(), new UnsignedInt128(1).hashCode());
    assertEquals(two.hashCode(), new UnsignedInt128(2).hashCode());
  }

  private void assertNotEquals(int a, int b) {
    assertTrue(a != b);
  }

  private void assertNotEquals(long a, long b) {
    assertTrue(a != b);
  }

  private void assertNotEquals(UnsignedInt128 a, UnsignedInt128 b) {
    assertTrue(!a.equals(b));
  }

  @Test
  public void testEquals() {
    assertNotEquals(one, two);
    assertNotEquals(zero, one);
    assertNotEquals(zero, two);

    assertEquals(zero, new UnsignedInt128(0));
    assertEquals(one, new UnsignedInt128(1));
    assertEquals(two, new UnsignedInt128(2));
  }

  @Test
  public void testCompareTo() {
    assertTrue(one.compareTo(two) < 0);
    assertTrue(two.compareTo(one) > 0);
    assertTrue(one.compareTo(zero) > 0);
    assertTrue(zero.compareTo(two) < 0);
  }

  @Test
  public void testCompareToScaleTen() {
    assertTrue(zero.compareToScaleTen(new UnsignedInt128(0), (short) 3) == 0);
    assertTrue(zero.compareToScaleTen(new UnsignedInt128(0), (short) -1) == 0);
    assertTrue(zero.compareToScaleTen(new UnsignedInt128(0), (short) 12) == 0);
    assertTrue(one.compareToScaleTen(zero, (short) 0) > 0);
    assertTrue(one.compareToScaleTen(zero, (short) 3) > 0);
    assertTrue(one.compareToScaleTen(zero, (short) -3) > 0);

    assertTrue(zero.compareToScaleTen(one, (short) 3) < 0);
    assertTrue(zero.compareToScaleTen(one, (short) 0) < 0);
    assertTrue(zero.compareToScaleTen(one, (short) -1) == 0);

    assertTrue(new UnsignedInt128(30).compareToScaleTen(new UnsignedInt128(3),
        (short) 1) == 0);
    assertTrue(new UnsignedInt128(30).compareToScaleTen(new UnsignedInt128(3),
        (short) 2) < 0);
    assertTrue(new UnsignedInt128(30).compareToScaleTen(new UnsignedInt128(3),
        (short) 0) > 0);

    assertTrue(new UnsignedInt128(680000000000L).compareToScaleTen(
        new UnsignedInt128(68), (short) 10) == 0);
    assertTrue(new UnsignedInt128(68).compareToScaleTen(new UnsignedInt128(
        680000000000L), (short) -10) == 0);
    assertTrue(new UnsignedInt128(680000000000L).compareToScaleTen(
        new UnsignedInt128(0), (short) 60) > 0);
    assertTrue(new UnsignedInt128(680000000000L).compareToScaleTen(
        new UnsignedInt128(0), (short) 30) > 0);
    assertTrue(new UnsignedInt128(680000000000L).compareToScaleTen(
        new UnsignedInt128(0), (short) 10) > 0);
    assertTrue(new UnsignedInt128(0).compareToScaleTen(new UnsignedInt128(
        680000000000L), (short) -10) < 0);
    assertTrue(new UnsignedInt128(0).compareToScaleTen(new UnsignedInt128(
        680000000000L), (short) -11) < 0);
    assertTrue(new UnsignedInt128(0).compareToScaleTen(new UnsignedInt128(
        680000000000L), (short) -12) < 0);
    assertTrue(new UnsignedInt128(0).compareToScaleTen(new UnsignedInt128(
        680000000000L), (short) -13) == 0);
    assertTrue(new UnsignedInt128(0).compareToScaleTen(new UnsignedInt128(
        680000000000L), (short) -30) == 0);

    assertTrue(new UnsignedInt128(680000000000L).compareToScaleTen(
        new UnsignedInt128(680000000001L), (short) 0) < 1);
    assertTrue(new UnsignedInt128(68000000000L).compareToScaleTen(
        new UnsignedInt128(680000000001L), (short) -1) == 0);
    assertTrue(new UnsignedInt128(68000000000L).compareToScaleTen(
        new UnsignedInt128(680000000000L), (short) -1) == 0);
    assertTrue(new UnsignedInt128(68000000000L).compareToScaleTen(
        new UnsignedInt128(679999999999L), (short) -1) == 0);

    assertTrue(new UnsignedInt128(0x10000000000000L).shiftLeftConstructive(32)
        .compareToScaleTen(
            new UnsignedInt128(0xA0000000000000L).shiftLeftConstructive(32),
            (short) -1) == 0);
    assertTrue(new UnsignedInt128(0x10000000000000L).shiftLeftConstructive(32)
        .compareToScaleTen(
            new UnsignedInt128(0xA0000000000000L).shiftLeftConstructive(32),
            (short) 1) < 0);
    assertTrue(new UnsignedInt128(0x10000000000000L).shiftLeftConstructive(32)
        .compareToScaleTen(
            new UnsignedInt128(0xA0000000000000L).shiftLeftConstructive(32),
            (short) 0) < 0);
    assertTrue(new UnsignedInt128(0x10000000000000L).shiftLeftConstructive(32)
        .compareToScaleTen(
            new UnsignedInt128(0xA0000000000000L).shiftLeftConstructive(32),
            (short) -2) > 0);
  }

  @Test
  public void testToFormalString() {
    assertEquals("0", zero.toFormalString());
    assertEquals("1", one.toFormalString());

    assertEquals("30", new UnsignedInt128(30).toFormalString());
    assertEquals("680000000000",
        new UnsignedInt128(680000000000L).toFormalString());
    assertEquals("6800000000000",
        new UnsignedInt128(6800000000000L).toFormalString());
    assertEquals("68", new UnsignedInt128(68).toFormalString());

    assertEquals(zero, new UnsignedInt128("0"));
    assertEquals(one, new UnsignedInt128("1"));

    assertEquals(new UnsignedInt128(30), new UnsignedInt128("30"));
    assertEquals(new UnsignedInt128(680000000000L), new UnsignedInt128(
        "680000000000"));
    assertEquals(new UnsignedInt128(6800000000000L), new UnsignedInt128(
        "6800000000000"));
    assertEquals(new UnsignedInt128(68), new UnsignedInt128("68"));
  }

  @Test
  public void testUnsignedInt128() {
    assertEquals(0L, new UnsignedInt128().asLong());
  }

  @Test
  public void testUnsignedInt128UnsignedInt128() {
    assertEquals(1L, new UnsignedInt128(one).asLong());
    assertEquals(2L, new UnsignedInt128(two).asLong());
  }

  @Test
  public void testUnsignedInt128IntIntIntInt() {
    assertEquals(((long) 11) << 32L | 23L,
        new UnsignedInt128(23, 11, 0, 0).asLong());
  }

  @Test
  public void testZeroClear() {
    assertFalse(one.isZero());
    assertFalse(two.isZero());
    assertNotEquals(0L, one.asLong());
    assertNotEquals(0L, two.asLong());

    two.zeroClear();

    assertNotEquals(0L, one.asLong());
    assertEquals(0L, two.asLong());
    assertFalse(one.isZero());
    assertTrue(two.isZero());

    one.zeroClear();

    assertEquals(0L, one.asLong());
    assertEquals(0L, two.asLong());
    assertTrue(one.isZero());
    assertTrue(two.isZero());
  }

  @Test
  public void testAddDestructive() {
    one.addDestructive(two);
    assertEquals(3L, one.asLong());
    assertEquals(2L, two.asLong());

    UnsignedInt128 big = new UnsignedInt128((1L << 62) + 3L);
    UnsignedInt128 tmp = new UnsignedInt128(0L);
    for (int i = 0; i < 54; ++i) {
      tmp.addDestructive(big);
    }

    assertEquals(3 * 54, tmp.getV0());
    assertEquals(0x80000000, tmp.getV1()); // (54 % 4) << 62
    assertEquals(13, tmp.getV2()); // 54/4
    assertEquals(0, tmp.getV3());

    assertEquals((1L << 62) + 3L, big.asLong());

    UnsignedInt128 huge = one.shiftLeftConstructive(127);
    UnsignedInt128 huge2 = one.shiftLeftConstructive(127);
    try {
      huge2.addDestructive(huge);
      fail();
    } catch (ArithmeticException ex) {
      // ok
    }
  }

  @Test
  public void testSubtractDestructive() {
    two.subtractDestructive(one);
    assertEquals(1L, one.asLong());
    assertEquals(1L, one.asLong());

    try {
      one.subtractDestructive(new UnsignedInt128(10L));
      fail();
    } catch (ArithmeticException ex) {
      // ok
    }

    UnsignedInt128 big = new UnsignedInt128((1L << 62) + (3L << 34) + 3L);
    big.shiftLeftDestructive(6);
    UnsignedInt128 tmp = new UnsignedInt128((1L << 61) + 5L);
    tmp.shiftLeftDestructive(6);

    big.subtractDestructive(tmp);
    big.subtractDestructive(tmp);

    assertEquals((3 << 6) - 2 * (5 << 6), big.getV0());
    assertEquals((3 << 8) - 1, big.getV1());
    assertEquals(0, big.getV2());
    assertEquals(0, big.getV3());
  }

  @Test
  public void testMultiplyDestructiveInt() {
    two.multiplyDestructive(1);
    assertEquals(2L, two.asLong());
    assertEquals(1L, one.asLong());
    two.multiplyDestructive(2);
    assertEquals(4L, two.asLong());

    UnsignedInt128 five = new UnsignedInt128(5);
    five.multiplyDestructive(6432346);
    assertEquals(6432346 * 5, five.getV0());
    assertEquals(0, five.getV1());
    assertEquals(0, five.getV2());
    assertEquals(0, five.getV3());

    UnsignedInt128 big = new UnsignedInt128((1L << 62) + (3L << 34) + 3L);
    big.multiplyDestructive(96);

    assertEquals(3 * 96, big.getV0());
    assertEquals(96 * (3 << 2), big.getV1());
    assertEquals(96 / 4, big.getV2());
    assertEquals(0, big.getV3());

    UnsignedInt128 tmp = new UnsignedInt128(1);
    tmp.shiftLeftDestructive(126);
    tmp.multiplyDestructive(2);
    try {
      tmp.multiplyDestructive(2);
      fail();
    } catch (ArithmeticException ex) {
      // ok
    }
  }

  @Test
  public void testShiftDestructive() {
    UnsignedInt128 big = new UnsignedInt128((1L << 62) + (23L << 32) + 89L);
    big.shiftLeftDestructive(2);

    assertEquals(89 * 4, big.getV0());
    assertEquals(23 * 4, big.getV1());
    assertEquals(1, big.getV2());
    assertEquals(0, big.getV3());

    big.shiftLeftDestructive(32);

    assertEquals(0, big.getV0());
    assertEquals(89 * 4, big.getV1());
    assertEquals(23 * 4, big.getV2());
    assertEquals(1, big.getV3());

    big.shiftRightDestructive(2, true);

    assertEquals(0, big.getV0());
    assertEquals(89, big.getV1());
    assertEquals(23 + (1 << 30), big.getV2());
    assertEquals(0, big.getV3());

    big.shiftRightDestructive(32, true);

    assertEquals(89, big.getV0());
    assertEquals(23 + (1 << 30), big.getV1());
    assertEquals(0, big.getV2());
    assertEquals(0, big.getV3());

    // test rounding
    UnsignedInt128 tmp = new UnsignedInt128(17);
    assertEquals(17, tmp.getV0());
    tmp.shiftRightDestructive(1, true);
    assertEquals(9, tmp.getV0());
    tmp.shiftRightDestructive(1, false);
    assertEquals(4, tmp.getV0());
    tmp.shiftRightDestructive(1, true);
    assertEquals(2, tmp.getV0());
    tmp.shiftRightDestructive(1, true);
    assertEquals(1, tmp.getV0());
    tmp.shiftRightDestructive(1, true);
    assertEquals(1, tmp.getV0());
    tmp.shiftRightDestructive(1, false);
    assertEquals(0, tmp.getV0());
  }

  @Test
  public void testMultiplyDestructiveUnsignedInt128() {
    two.multiplyDestructive(one);
    assertEquals(2L, two.asLong());
    assertEquals(1L, one.asLong());
    two.multiplyDestructive(two);
    assertEquals(4L, two.asLong());

    UnsignedInt128 five = new UnsignedInt128(5);
    five.multiplyDestructive(new UnsignedInt128(6432346));
    assertEquals(6432346 * 5, five.getV0());
    assertEquals(0, five.getV1());
    assertEquals(0, five.getV2());
    assertEquals(0, five.getV3());

    UnsignedInt128 big = new UnsignedInt128((1L << 62) + (3L << 34) + 3L);
    big.multiplyDestructive(new UnsignedInt128(96));

    assertEquals(3 * 96, big.getV0());
    assertEquals(96 * (3 << 2), big.getV1());
    assertEquals(96 / 4, big.getV2());
    assertEquals(0, big.getV3());

    UnsignedInt128 tmp = new UnsignedInt128(1);
    tmp.shiftLeftDestructive(126);
    tmp.multiplyDestructive(new UnsignedInt128(2));
    try {
      tmp.multiplyDestructive(new UnsignedInt128(2));
      fail();
    } catch (ArithmeticException ex) {
      // ok
    }

    UnsignedInt128 complicated1 = new UnsignedInt128(0xF9892FCA, 0x59D109AD,
        0x0534AB4C, 0);
    BigInteger bigInteger1 = complicated1.toBigIntegerSlow();
    UnsignedInt128 complicated2 = new UnsignedInt128(54234234, 9, 0, 0);
    BigInteger bigInteger2 = complicated2.toBigIntegerSlow();
    complicated1.multiplyDestructive(complicated2);
    BigInteger ans = bigInteger1.multiply(bigInteger2);
    assertEquals(ans, complicated1.toBigIntegerSlow());

    try {
      UnsignedInt128 complicated3 = new UnsignedInt128(0xF9892FCA, 0x59D109AD,
          0x0534AB4C, 0);
      complicated3
          .multiplyDestructive(new UnsignedInt128(54234234, 9845, 0, 0));
      fail();
    } catch (ArithmeticException ex) {
      // ok
    }
  }

  @Test
  public void testMultiplyScaleDownTenDestructiveScaleTen() {
    for (int scale = 0; scale < 38; ++scale) {
      UnsignedInt128 right = new UnsignedInt128(1);
      right.scaleUpTenDestructive((short) scale);
      {
        // 10000000....000
        UnsignedInt128 leftJust = new UnsignedInt128(1);
        leftJust.scaleUpTenDestructive((short) 15);
        UnsignedInt128 leftInc = leftJust.incrementConstructive();
        UnsignedInt128 leftDec = leftJust.decrementConstructive();

        if (scale + 10 <= 38) {
          leftJust.multiplyScaleDownTenDestructive(right, (short) (scale + 10));
          assertEquals("scale=" + scale, 100000L, leftJust.asLong());
          leftInc.multiplyScaleDownTenDestructive(right, (short) (scale + 10));
          assertEquals("scale=" + scale, 100000L, leftInc.asLong());
          leftDec.multiplyScaleDownTenDestructive(right, (short) (scale + 10));
          assertEquals("scale=" + scale, 100000L, leftDec.asLong());
        } else {
          leftJust.multiplyScaleDownTenDestructive(right, (short) (scale + 10));
          assertEquals("scale=" + scale, 0L, leftJust.asLong());
          leftInc.multiplyScaleDownTenDestructive(right, (short) (scale + 10));
          assertEquals("scale=" + scale, 0L, leftInc.asLong());
          leftDec.multiplyScaleDownTenDestructive(right, (short) (scale + 10));
          assertEquals("scale=" + scale, 0L, leftDec.asLong());
        }
      }

      {
        // 10000500....00
        UnsignedInt128 leftHalfJust = new UnsignedInt128(1);
        leftHalfJust.scaleUpTenDestructive((short) 6);
        leftHalfJust.addDestructive(new UnsignedInt128(5));
        leftHalfJust.scaleUpTenDestructive((short) 9);
        UnsignedInt128 leftHalfInc = leftHalfJust.incrementConstructive();
        UnsignedInt128 leftHalfDec = leftHalfJust.decrementConstructive();

        if (scale + 10 <= 38) {
          leftHalfJust.multiplyScaleDownTenDestructive(right,
              (short) (scale + 10));
          assertEquals("scale=" + scale, 100001L, leftHalfJust.asLong());
          leftHalfInc.multiplyScaleDownTenDestructive(right,
              (short) (scale + 10));
          assertEquals("scale=" + scale, 100001L, leftHalfInc.asLong());
          leftHalfDec.multiplyScaleDownTenDestructive(right,
              (short) (scale + 10));
          assertEquals("scale=" + scale, 100000L, leftHalfDec.asLong());
        } else {
          leftHalfJust.multiplyScaleDownTenDestructive(right,
              (short) (scale + 10));
          assertEquals("scale=" + scale, 0L, leftHalfJust.asLong());
          leftHalfInc.multiplyScaleDownTenDestructive(right,
              (short) (scale + 10));
          assertEquals("scale=" + scale, 0L, leftHalfInc.asLong());
          leftHalfDec.multiplyScaleDownTenDestructive(right,
              (short) (scale + 10));
          assertEquals("scale=" + scale, 0L, leftHalfDec.asLong());
        }
      }
    }
  }

  @Test
  public void testDivideDestructiveInt() {
    two.divideDestructive(1);
    assertEquals(1L, one.asLong());
    assertEquals(2L, two.asLong());
    one.divideDestructive(2);
    assertEquals(0L, one.asLong());
    assertEquals(2L, two.asLong());

    UnsignedInt128 var1 = new UnsignedInt128(1234234662345L);
    var1.divideDestructive(642337);
    assertEquals(1234234662345L / 642337L, var1.asLong());

    UnsignedInt128 complicated1 = new UnsignedInt128(0xF9892FCA, 0x59D109AD,
        0x0534AB4C, 0);
    BigInteger bigInteger1 = complicated1.toBigIntegerSlow();
    complicated1.divideDestructive(1534223465);
    BigInteger bigInteger2 = BigInteger.valueOf(1534223465);
    BigInteger ans = bigInteger1.divide(bigInteger2);
    assertEquals(ans, complicated1.toBigIntegerSlow());

    try {
      complicated1.divideDestructive(0);
      fail();
    } catch (ArithmeticException ex) {
      // ok
    }
  }

  @Test
  public void testDivideDestructiveUnsignedInt128() {
    UnsignedInt128 remainder = new UnsignedInt128();
    two.divideDestructive(one, remainder);
    assertEquals(1L, one.asLong());
    assertEquals(2L, two.asLong());
    assertEquals(zero, remainder);
    one.divideDestructive(two, remainder);
    assertEquals(0L, one.asLong());
    assertEquals(2L, two.asLong());
    assertEquals(new UnsignedInt128(1), remainder);

    UnsignedInt128 var1 = new UnsignedInt128(1234234662345L);
    var1.divideDestructive(new UnsignedInt128(642337), remainder);
    assertEquals(1234234662345L / 642337L, var1.asLong());
    assertEquals(1234234662345L % 642337L, remainder.asLong());

    UnsignedInt128 complicated1 = new UnsignedInt128(0xF9892FCA, 0x59D109AD,
        0x0534AB4C, 0x42395ADC);
    UnsignedInt128 complicated2 = new UnsignedInt128(0xF09DC19A, 0x00001234, 0,
        0);
    BigInteger bigInteger1 = complicated1.toBigIntegerSlow();
    BigInteger bigInteger2 = complicated2.toBigIntegerSlow();
    complicated1.divideDestructive(complicated2, remainder);
    BigInteger ans = bigInteger1.divide(bigInteger2);
    assertEquals(ans, complicated1.toBigIntegerSlow());

    try {
      complicated1.divideDestructive(zero, remainder);
      fail();
    } catch (ArithmeticException ex) {
      // ok
    }
  }

  @Test
  public void testDivideDestructiveUnsignedInt128Again() {
    UnsignedInt128 complicated1 = new UnsignedInt128(0xF9892FCA, 0x59D109AD, 0,
        0);
    UnsignedInt128 complicated2 = new UnsignedInt128(0xF09DC19A, 3, 0, 0);
    BigInteger bigInteger1 = complicated1.toBigIntegerSlow();
    BigInteger bigInteger2 = complicated2.toBigIntegerSlow();
    complicated1.divideDestructive(complicated2, new UnsignedInt128());
    BigInteger ans = bigInteger1.divide(bigInteger2);
    assertEquals(ans, complicated1.toBigIntegerSlow());
  }

  @Test
  public void testBigIntConversion() {
    BigInteger bigInteger = BigInteger.valueOf(0x1ABCDEF0123456L);
    UnsignedInt128 uInt128 = new UnsignedInt128(bigInteger);
    assertEquals(bigInteger, uInt128.toBigIntegerSlow());
  }
}
