/*
 * 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 java.util.Random;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.math.BigDecimal;
import java.math.BigInteger;

import org.apache.orc.impl.SerializationUtils;
import org.apache.hadoop.hive.common.type.RandomTypeUtil;
import org.apache.hadoop.hive.ql.exec.vector.expressions.StringExpr;

import com.google.code.tempusfugit.concurrency.annotations.*;

import org.junit.*;

import static org.junit.Assert.*;

public class TestHiveDecimalOrcSerializationUtils extends HiveDecimalTestBase {

  //------------------------------------------------------------------------------------------------

  @Test
  @Concurrent(count=4)
  public void testSerializationUtilsWriteRead() {
    testSerializationUtilsWriteRead("0.00");
    testSerializationUtilsWriteRead("1");
    testSerializationUtilsWriteRead("234.79");
    testSerializationUtilsWriteRead("-12.25");
    testSerializationUtilsWriteRead("99999999999999999999999999999999");
    testSerializationUtilsWriteRead("-99999999999999999999999999999999");
    testSerializationUtilsWriteRead("99999999999999999999999999999999999999");
    //                               12345678901234567890123456789012345678
    testSerializationUtilsWriteRead("-99999999999999999999999999999999999999");
    testSerializationUtilsWriteRead("999999999999.99999999999999999999");
    testSerializationUtilsWriteRead("-999999.99999999999999999999999999");
    testSerializationUtilsWriteRead("9999999999999999999999.9999999999999999");
    testSerializationUtilsWriteRead("-9999999999999999999999999999999.9999999");

    testSerializationUtilsWriteRead("4611686018427387903");  // 2^62 - 1
    testSerializationUtilsWriteRead("-4611686018427387903");
    testSerializationUtilsWriteRead("4611686018427387904");  // 2^62
    testSerializationUtilsWriteRead("-4611686018427387904");

    testSerializationUtilsWriteRead("42535295865117307932921825928971026431");  // 2^62*2^63 - 1
    testSerializationUtilsWriteRead("-42535295865117307932921825928971026431");
    testSerializationUtilsWriteRead("42535295865117307932921825928971026432");  // 2^62*2^63
    testSerializationUtilsWriteRead("-42535295865117307932921825928971026432");

    testSerializationUtilsWriteRead("54216721532321902598.70");
    testSerializationUtilsWriteRead("-906.62545207002374150309544832320");
  }

  private void testSerializationUtilsWriteRead(String string) {
    // System.out.println("TEST_FAST_SERIALIZATION_UTILS_WRITE_BIG_INTEGER ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ START ~~~~~~~~~~~~~~~~~");

    HiveDecimal dec = HiveDecimal.create(string);
    assertTrue(dec != null);
    // System.out.println("TEST_FAST_SERIALIZATION_UTILS_WRITE_BIG_INTEGER dec " + dec.toString());

    BigInteger bigInteger = dec.unscaledValue();
    int scale = dec.scale();
    // System.out.println("TEST_FAST_SERIALIZATION_UTILS_WRITE_BIG_INTEGER bigInteger " + bigInteger.toString());
    // System.out.println("TEST_FAST_SERIALIZATION_UTILS_WRITE_BIG_INTEGER scale " + scale);

    //---------------------------------------------------
    HiveDecimalV1 oldDec = HiveDecimalV1.create(string);
    assertTrue(oldDec != null);
    // System.out.println("TEST_FAST_SERIALIZATION_UTILS_WRITE_BIG_INTEGER oldDec " + oldDec.toString());

    BigInteger oldBigInteger = oldDec.unscaledValue();
    int oldScale = oldDec.scale();
    // System.out.println("TEST_FAST_SERIALIZATION_UTILS_WRITE_BIG_INTEGER oldBigInteger " + oldBigInteger.toString());
    // System.out.println("TEST_FAST_SERIALIZATION_UTILS_WRITE_BIG_INTEGER oldScale " + oldScale);
    //---------------------------------------------------

    long[] scratchLongs = new long[HiveDecimal.SCRATCH_LONGS_LEN];

    int which = 0;
    try {
      ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
      if (!dec.serializationUtilsWrite(
          outputStream, scratchLongs)) {
        // System.out.println("TEST_FAST_SERIALIZATION_UTILS_WRITE_BIG_INTEGER serializationUtilsWrite failed");
        fail();
      }
      byte[] bytes = outputStream.toByteArray();
  
      ByteArrayOutputStream outputStreamExpected = new ByteArrayOutputStream();
      SerializationUtils.writeBigInteger(outputStreamExpected, bigInteger);
      byte[] bytesExpected = outputStreamExpected.toByteArray();
  
      // System.out.println("TEST_FAST_SERIALIZATION_UTILS_WRITE_BIG_INTEGER check streams");
      // System.out.println("TEST_FAST_SERIALIZATION_UTILS_WRITE_BIG_INTEGER bytes1        " + displayBytes(bytes, 0, bytes.length));
      if (!StringExpr.equal(bytes, 0, bytes.length, bytesExpected, 0, bytesExpected.length)) {
        // Tailing zeroes difference ok.
        // System.out.println("TEST_FAST_SERIALIZATION_UTILS_WRITE_BIG_INTEGER streams not equal");
        // System.out.println("TEST_FAST_SERIALIZATION_UTILS_WRITE_BIG_INTEGER bytesExpected " + displayBytes(bytesExpected, 0, bytesExpected.length));
      }
      // Deserialize and check...
      which = 1;
      ByteArrayInputStream byteArrayInputStream = new ByteArrayInputStream(bytes);
      BigInteger deserializedBigInteger = SerializationUtils.readBigInteger(byteArrayInputStream);

      which = 2;
      ByteArrayInputStream byteArrayInputStreamExpected = new ByteArrayInputStream(bytesExpected);
      BigInteger deserializedBigIntegerExpected = SerializationUtils.readBigInteger(byteArrayInputStreamExpected);
      // System.out.println("TEST_FAST_SERIALIZATION_UTILS_WRITE_BIG_INTEGER deserialized equals " +
      //    deserializedBigInteger.equals(deserializedBigIntegerExpected));
      if (!deserializedBigInteger.equals(deserializedBigIntegerExpected)) {
        // System.out.println("TEST_FAST_SERIALIZATION_UTILS_WRITE_BIG_INTEGER deserializedBigInteger " + deserializedBigInteger.toString() +
        //    " deserializedBigIntegerExpected " + deserializedBigIntegerExpected.toString());
        fail();
      }

      which = 3;
      ByteArrayInputStream byteArrayInputStreamRead = new ByteArrayInputStream(bytes);
      byte[] scratchBytes = new byte[HiveDecimal.SCRATCH_BUFFER_LEN_SERIALIZATION_UTILS_READ];
      HiveDecimal readHiveDecimal =
          HiveDecimal.serializationUtilsRead(byteArrayInputStreamRead, scale, scratchBytes);
      assertTrue(readHiveDecimal != null);
      // System.out.println("TEST_FAST_SERIALIZATION_UTILS_WRITE_BIG_INTEGER read readHiveDecimal " + readHiveDecimal.toString() +
      //    " dec " + dec.toString() + " (scale parameter " + scale + ")");
      // System.out.println("TEST_FAST_SERIALIZATION_UTILS_WRITE_BIG_INTEGER read toString equals " +
      //    readHiveDecimal.toString().equals(dec.toString()));
      assertEquals(readHiveDecimal.toString(), dec.toString());
      // System.out.println("TEST_FAST_SERIALIZATION_UTILS_WRITE_BIG_INTEGER read equals " +
      //    readHiveDecimal.equals(dec));
      assertEquals(readHiveDecimal, dec);
    } catch (IOException e) {
      // System.out.println("TEST_FAST_SERIALIZATION_UTILS_WRITE_BIG_INTEGER " + e + " which " + which);
      fail();
    }
    // System.out.println("TEST_FAST_SERIALIZATION_UTILS_WRITE_BIG_INTEGER ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~  END  ~~~~~~~~~~~~~~~~~");

  }

  //------------------------------------------------------------------------------------------------

  @Test
  public void testRandomSerializationUtilsRead()
      throws IOException {
    doTestRandomSerializationUtilsRead(standardAlphabet);
  }

  @Test
  public void testRandomSerializationUtilsReadSparse()
      throws IOException {
    for (String digitAlphabet : sparseAlphabets) {
      doTestRandomSerializationUtilsRead(digitAlphabet);
    }
  }

  private void doTestRandomSerializationUtilsRead(String digitAlphabet)
      throws IOException {

    Random r = new Random(2389);
    for (int i = 0; i < POUND_FACTOR; i++) {
      BigInteger bigInteger = randHiveBigInteger(r, digitAlphabet);

      doTestSerializationUtilsRead(r, bigInteger);
    }
  }

  @Test
  public void testSerializationUtilsReadSpecial()
      throws IOException {
    Random r = new Random(9923);
    for (BigDecimal bigDecimal : specialBigDecimals) {
      doTestSerializationUtilsRead(r, bigDecimal.unscaledValue());
    }
  }

  private void doTestSerializationUtilsRead(Random r, BigInteger bigInteger)
     throws IOException {

    // System.out.println("TEST_SERIALIZATION_UTILS_READ bigInteger " + bigInteger);

    HiveDecimalV1 oldDec = HiveDecimalV1.create(bigInteger);
    if (oldDec != null && isTenPowerBug(oldDec.toString())) {
      return;
    }
    HiveDecimal dec = HiveDecimal.create(bigInteger);
    if (oldDec == null) {
      assertTrue(dec == null);
      return;
    }
    assertTrue(dec != null);
    dec.validate();
    // System.out.println("TEST_SERIALIZATION_UTILS_READ oldDec " + oldDec);
    // System.out.println("TEST_SERIALIZATION_UTILS_READ dec " + dec);

    Assert.assertEquals(bigInteger, oldDec.unscaledValue());
    Assert.assertEquals(bigInteger, dec.unscaledValue());

    ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
    SerializationUtils.writeBigInteger(outputStream, bigInteger);
    byte[] bytes = outputStream.toByteArray();

    ByteArrayInputStream byteArrayInputStream = new ByteArrayInputStream(bytes);
    BigInteger deserializedBigInteger =
        SerializationUtils.readBigInteger(byteArrayInputStream);

    // Verify SerializationUtils first.
    Assert.assertEquals(bigInteger, deserializedBigInteger);

    // Now HiveDecimal
    byte[] scratchBytes = new byte[HiveDecimal.SCRATCH_BUFFER_LEN_SERIALIZATION_UTILS_READ];

    byteArrayInputStream = new ByteArrayInputStream(bytes);
    HiveDecimal resultDec =
        dec.serializationUtilsRead(
            byteArrayInputStream, dec.scale(),
            scratchBytes);
    assertTrue(resultDec != null);
    resultDec.validate();

    Assert.assertEquals(dec.toString(), resultDec.toString());

    //----------------------------------------------------------------------------------------------

    // Add scale.

    int scale = 0 + r.nextInt(38 + 1);
    BigDecimal bigDecimal = new BigDecimal(bigInteger, scale);

    oldDec = HiveDecimalV1.create(bigDecimal);
    dec = HiveDecimal.create(bigDecimal);
    if (oldDec == null) {
      assertTrue(dec == null);
      return;
    }
    assertTrue(dec != null);
    dec.validate();
    // System.out.println("TEST_SERIALIZATION_UTILS_READ with scale oldDec " + oldDec);
    // System.out.println("TEST_SERIALIZATION_UTILS_READ with scale dec " + dec);

    outputStream = new ByteArrayOutputStream();
    SerializationUtils.writeBigInteger(outputStream, dec.unscaledValue());
    bytes = outputStream.toByteArray();

    // Now HiveDecimal
    byteArrayInputStream = new ByteArrayInputStream(bytes);
    resultDec =
        dec.serializationUtilsRead(
            byteArrayInputStream, dec.scale(),
            scratchBytes);
    assertTrue(resultDec != null);
    resultDec.validate();

    Assert.assertEquals(dec.toString(), resultDec.toString());
  }

  //------------------------------------------------------------------------------------------------

  @Test
  public void testRandomSerializationUtilsWrite()
      throws IOException {
    doTestRandomSerializationUtilsWrite(standardAlphabet, false);
  }

  @Test
  public void testRandomSerializationUtilsWriteFractionsOnly()
      throws IOException {
    doTestRandomSerializationUtilsWrite(standardAlphabet, true);
  }

  @Test
  public void testRandomSerializationUtilsWriteSparse()
      throws IOException {
    for (String digitAlphabet : sparseAlphabets) {
      doTestRandomSerializationUtilsWrite(digitAlphabet, false);
    }
  }

  private void doTestRandomSerializationUtilsWrite(String digitAlphabet, boolean fractionsOnly)
      throws IOException {

    Random r = new Random(823);
    for (int i = 0; i < POUND_FACTOR; i++) {
      BigInteger bigInteger = randHiveBigInteger(r, digitAlphabet);

      doTestSerializationUtilsWrite(r, bigInteger);
    }
  }

  @Test
  public void testSerializationUtilsWriteSpecial()
      throws IOException {
    Random r = new Random(998737);
    for (BigDecimal bigDecimal : specialBigDecimals) {
      doTestSerializationUtilsWrite(r, bigDecimal.unscaledValue());
    }
  }

  private void doTestSerializationUtilsWrite(Random r, BigInteger bigInteger)
     throws IOException {

    // System.out.println("TEST_SERIALIZATION_UTILS_WRITE bigInteger " + bigInteger);

    HiveDecimalV1 oldDec = HiveDecimalV1.create(bigInteger);
    if (oldDec != null && isTenPowerBug(oldDec.toString())) {
      return;
    }
    HiveDecimal dec = HiveDecimal.create(bigInteger);
    if (oldDec == null) {
      assertTrue(dec == null);
      return;
    }
    assertTrue(dec != null);
    dec.validate();
    // System.out.println("TEST_SERIALIZATION_UTILS_WRITE oldDec " + oldDec);
    // System.out.println("TEST_SERIALIZATION_UTILS_WRITE dec " + dec);

    Assert.assertEquals(bigInteger, oldDec.unscaledValue());
    Assert.assertEquals(bigInteger, dec.unscaledValue());

    ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
    SerializationUtils.writeBigInteger(outputStream, bigInteger);
    byte[] bytes = outputStream.toByteArray();

    ByteArrayInputStream byteArrayInputStream = new ByteArrayInputStream(bytes);
    BigInteger deserializedBigInteger =
        SerializationUtils.readBigInteger(byteArrayInputStream);

    // Verify SerializationUtils first.
    Assert.assertEquals(bigInteger, deserializedBigInteger);

    ByteArrayOutputStream decOutputStream = new ByteArrayOutputStream();

    long[] scratchLongs = new long[HiveDecimal.SCRATCH_LONGS_LEN];

    boolean successful =
        dec.serializationUtilsWrite(
            decOutputStream, scratchLongs);
    Assert.assertTrue(successful);
    byte[] decBytes = decOutputStream.toByteArray();

    if (!StringExpr.equal(bytes, 0, bytes.length, decBytes, 0, decBytes.length)) {
      // Tailing zeroes difference ok...
      // System.out.println("TEST_SERIALIZATION_UTILS_WRITE streams not equal");
      // System.out.println("TEST_SERIALIZATION_UTILS_WRITE bytes " + displayBytes(bytes, 0, bytes.length));
      // System.out.println("TEST_SERIALIZATION_UTILS_WRITE decBytes " + displayBytes(decBytes, 0, decBytes.length));
    }

    ByteArrayInputStream decByteArrayInputStream = new ByteArrayInputStream(decBytes);
    BigInteger decDeserializedBigInteger =
        SerializationUtils.readBigInteger(decByteArrayInputStream);

    Assert.assertEquals(bigInteger, decDeserializedBigInteger);
  }
}