/*
 * 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.datasketches.theta;

import static java.lang.foreign.ValueLayout.JAVA_BYTE;
import static java.lang.foreign.ValueLayout.JAVA_LONG_UNALIGNED;
import static org.apache.datasketches.hash.MurmurHash3.hash;
import static org.testng.Assert.assertEquals;
import static org.testng.Assert.assertFalse;
import static org.testng.Assert.assertNull;
import static org.testng.Assert.assertTrue;
import static org.testng.Assert.fail;

import java.lang.foreign.MemorySegment;

import org.apache.datasketches.common.SketchesArgumentException;
import org.apache.datasketches.common.Util;
import org.testng.annotations.Test;

/**
 * @author Lee Rhodes
 */
public class SingleItemSketchTest {
  final static short DEFAULT_SEED_HASH = (short) (Util.computeSeedHash(Util.DEFAULT_UPDATE_SEED) & 0XFFFFL);

  @Test
  public void check1() {
    final ThetaUnion union = ThetaSetOperation.builder().buildUnion();
    union.union(SingleItemSketch.create(1));
    union.union(SingleItemSketch.create(1.0));
    union.union(SingleItemSketch.create(0.0));
    union.union(SingleItemSketch.create("1"));
    union.union(SingleItemSketch.create(new byte[] {1,2,3,4}));
    union.union(SingleItemSketch.create(new char[] {'a'}));
    union.union(SingleItemSketch.create(new int[] {2}));
    union.union(SingleItemSketch.create(new long[] {3}));

    union.union(SingleItemSketch.create(-0.0)); //duplicate

    final double est = union.getResult().getEstimate();
    println(""+est);
    assertEquals(est, 8.0, 0.0);

    assertNull(SingleItemSketch.create(""));
    final String str = null;
    assertNull(SingleItemSketch.create(str));//returns null

    assertNull(SingleItemSketch.create(new byte[0]));//returns null
    final byte[] byteArr = null;
    assertNull(SingleItemSketch.create(byteArr));//returns null

    assertNull(SingleItemSketch.create(new char[0]));//returns null
    final char[] charArr = null;
    assertNull(SingleItemSketch.create(charArr));//returns null

    assertNull(SingleItemSketch.create(new int[0]));//returns null
    final int[] intArr = null;
    assertNull(SingleItemSketch.create(intArr));//returns null

    assertNull(SingleItemSketch.create(new long[0]));//returns null
    final long[] longArr = null;
    assertNull(SingleItemSketch.create(longArr));//returns null
  }

  @Test
  public void check2() {
    final long seed = Util.DEFAULT_UPDATE_SEED;
    final ThetaUnion union = ThetaSetOperation.builder().buildUnion();
    union.union(SingleItemSketch.create(1, seed));
    union.union(SingleItemSketch.create(1.0, seed));
    union.union(SingleItemSketch.create(0.0, seed));
    union.union(SingleItemSketch.create("1", seed));
    union.union(SingleItemSketch.create(new byte[] {1,2,3,4}, seed));
    union.union(SingleItemSketch.create(new char[] {'a'}, seed));
    union.union(SingleItemSketch.create(new int[] {2}, seed));
    union.union(SingleItemSketch.create(new long[] {3}, seed));

    union.union(SingleItemSketch.create(-0.0, seed)); //duplicate

    final double est = union.getResult().getEstimate();
    println(""+est);
    assertEquals(est, 8.0, 0.0);

    assertNull(SingleItemSketch.create("", seed));
    final String str = null;
    assertNull(SingleItemSketch.create(str, seed));//returns null

    assertNull(SingleItemSketch.create(new byte[0], seed));//returns null
    final byte[] byteArr = null;
    assertNull(SingleItemSketch.create(byteArr, seed));//returns null

    assertNull(SingleItemSketch.create(new char[0], seed));//returns null
    final char[] charArr = null;
    assertNull(SingleItemSketch.create(charArr, seed));//returns null

    assertNull(SingleItemSketch.create(new int[0], seed));//returns null
    final int[] intArr = null;
    assertNull(SingleItemSketch.create(intArr, seed));//returns null

    assertNull(SingleItemSketch.create(new long[0], seed));//returns null
    final long[] longArr = null;
    assertNull(SingleItemSketch.create(longArr, seed));//returns null
  }

  @Test
  public void checkSketchInterface() {
    final SingleItemSketch sis = SingleItemSketch.create(1);
    assertEquals(sis.getCompactBytes(), 16);
    assertEquals(sis.getEstimate(), 1.0);
    assertEquals(sis.getLowerBound(1), 1.0);
    assertEquals(sis.getRetainedEntries(true), 1);
    assertEquals(sis.getUpperBound(1), 1.0);
    assertFalse(sis.isOffHeap());
    assertFalse(sis.hasMemorySegment());
    assertFalse(sis.isEmpty());
    assertTrue(sis.isOrdered());
  }

  @Test
  public void checkLessThanThetaLong() {
    for (int i = 0; i < 10; i++) {
      final long[] data = { i };
      final long h = hash(data, Util.DEFAULT_UPDATE_SEED)[0] >>> 1;
      final SingleItemSketch sis = SingleItemSketch.create(i);
      final long halfMax = Long.MAX_VALUE >> 1;
      final int count = sis.getCountLessThanThetaLong(halfMax);
      assertEquals(count, (h < halfMax) ? 1 : 0);
    }
  }

  @Test
  public void checkSerDe() {
    final SingleItemSketch sis = SingleItemSketch.create(1);
    final byte[] byteArr = sis.toByteArray();
    final MemorySegment seg = MemorySegment.ofArray(byteArr);
    final short defaultSeedHash = Util.computeSeedHash(Util.DEFAULT_UPDATE_SEED);
    final SingleItemSketch sis2 = SingleItemSketch.heapify(seg,  defaultSeedHash);
    assertEquals(sis2.getEstimate(), 1.0);

    final SingleItemSketch sis3 = SingleItemSketch.heapify(seg , defaultSeedHash);
    assertEquals(sis3.getEstimate(), 1.0);

    final ThetaUnion union = ThetaSetOperation.builder().buildUnion();
    union.union(sis);
    union.union(sis2);
    union.union(sis3);
    final CompactThetaSketch csk = union.getResult();
    assertTrue(csk instanceof SingleItemSketch);
    assertEquals(union.getResult().getEstimate(), 1.0);
  }

  @Test
  public void checkRestricted() {
    final SingleItemSketch sis = SingleItemSketch.create(1);
    assertNull(sis.getMemorySegment());
    assertEquals(sis.getCompactPreambleLongs(), 1);
  }

  @Test
  public void unionWrapped() {
    final ThetaSketch sketch = SingleItemSketch.create(1);
    final ThetaUnion union = ThetaSetOperation.builder().buildUnion();
    final MemorySegment seg  = MemorySegment.ofArray(sketch.toByteArray());
    union.union(seg );
    assertEquals(union.getResult().getEstimate(), 1, 0);
  }

  @Test
  public void buildAndCompact() {
    UpdatableThetaSketch sk1;
    CompactThetaSketch csk;
    int bytes;
    //On-heap
    sk1 = UpdatableThetaSketch.builder().setNominalEntries(32).build();
    sk1.update(1);
    csk = sk1.compact(true, null);
    assertTrue(csk instanceof SingleItemSketch);
    csk = sk1.compact(false, null);
    assertTrue(csk instanceof SingleItemSketch);

    //Off-heap
    bytes = ThetaSketch.getMaxUpdateSketchBytes(32);
    MemorySegment wseg  = MemorySegment.ofArray(new byte[bytes]);
    sk1= UpdatableThetaSketch.builder().setNominalEntries(32).build(wseg );
    sk1.update(1);
    csk = sk1.compact(true, null);
    assertTrue(csk instanceof SingleItemSketch);
    csk = sk1.compact(false, null);
    assertTrue(csk instanceof SingleItemSketch);

    bytes = ThetaSketch.getMaxCompactSketchBytes(1);
    wseg  = MemorySegment.ofArray(new byte[bytes]);
    csk = sk1.compact(true, wseg );
    assertTrue(csk.isOrdered());
    csk = sk1.compact(false, wseg );
    assertTrue(csk.isOrdered());
  }

  @Test
  public void intersection() {
    UpdatableThetaSketch sk1, sk2;
    CompactThetaSketch csk;
    int bytes;
    //ThetaIntersection on-heap
    sk1 = UpdatableThetaSketch.builder().setNominalEntries(32).build();
    sk2 = UpdatableThetaSketch.builder().setNominalEntries(32).build();
    sk1.update(1);
    sk1.update(2);
    sk2.update(1);
    ThetaIntersection inter = ThetaSetOperation.builder().buildIntersection();
    inter.intersect(sk1);
    inter.intersect(sk2);
    csk = inter.getResult(true, null);
    assertTrue(csk instanceof SingleItemSketch);

    //ThetaIntersection off-heap
    bytes = ThetaSetOperation.getMaxIntersectionBytes(32);
    final MemorySegment wseg  = MemorySegment.ofArray(new byte[bytes]);
    inter = ThetaSetOperation.builder().buildIntersection(wseg );
    inter.intersect(sk1);
    inter.intersect(sk2);
    csk = inter.getResult(true, null);
    assertTrue(csk instanceof SingleItemSketch);
    csk = inter.getResult(false, null);
    assertTrue(csk instanceof SingleItemSketch);
  }

  @Test
  public void union() {
    UpdatableThetaSketch sk1, sk2;
    CompactThetaSketch csk;
    int bytes;
    //ThetaUnion on-heap
    sk1 = UpdatableThetaSketch.builder().setNominalEntries(32).build();
    sk2 = UpdatableThetaSketch.builder().setNominalEntries(32).build();
    sk1.update(1);
    sk2.update(1);
    ThetaUnion union = ThetaSetOperation.builder().buildUnion();
    union.union(sk1);
    union.union(sk2);
    csk = union.getResult(true, null);
    assertTrue(csk instanceof SingleItemSketch);

    //ThetaUnion off-heap
    bytes = ThetaSetOperation.getMaxUnionBytes(32);
    final MemorySegment wseg  = MemorySegment.ofArray(new byte[bytes]);
    union = ThetaSetOperation.builder().buildUnion(wseg );
    union.union(sk1);
    union.union(sk2);
    csk = union.getResult(true, null);
    assertTrue(csk instanceof SingleItemSketch);
    csk = union.getResult(false, null);
    assertTrue(csk instanceof SingleItemSketch);
  }

  @Test
  public void aNotB() {
    UpdatableThetaSketch sk1, sk2;
    CompactThetaSketch csk;
    //ThetaAnotB on-heap
    sk1 = UpdatableThetaSketch.builder().setNominalEntries(32).build();
    sk2 = UpdatableThetaSketch.builder().setNominalEntries(32).build();
    sk1.update(1);
    sk2.update(2);
    final ThetaAnotB aNotB = ThetaSetOperation.builder().buildANotB();
    aNotB.setA(sk1);
    aNotB.notB(sk2);
    csk = aNotB.getResult(true, null, true);
    assertTrue(csk instanceof SingleItemSketch);
    //not ThetaAnotB off-heap form
  }

  @Test
  public void checkHeapifyInstance() {
    final UpdatableThetaSketch sk1 = new UpdatableThetaSketchBuilder().build();
    sk1.update(1);
    final UpdatableThetaSketch sk2 = new UpdatableThetaSketchBuilder().build();
    sk2.update(1);
    final ThetaIntersection inter = ThetaSetOperation.builder().buildIntersection();
    inter.intersect(sk1);
    inter.intersect(sk2);
    final MemorySegment wseg  = MemorySegment.ofArray(new byte[16]);
    final CompactThetaSketch csk = inter.getResult(false, wseg );
    assertTrue(csk.isOrdered());
    final ThetaSketch csk2 = ThetaSketch.heapify(wseg );
    assertTrue(csk2 instanceof SingleItemSketch);
    println(csk2.toString(true, true, 1, true));
  }

  @Test
  public void checkSingleItemBadFlags() {
    final short defaultSeedHash = Util.computeSeedHash(Util.DEFAULT_UPDATE_SEED);
    final UpdatableThetaSketch sk1 = new UpdatableThetaSketchBuilder().build();
    sk1.update(1);
    final MemorySegment wseg  = MemorySegment.ofArray(new byte[16]);
    sk1.compact(true, wseg );
    wseg .set(JAVA_BYTE, 5, (byte) 0); //corrupt flags to zero
    try {
      SingleItemSketch.heapify(wseg , defaultSeedHash); //fails due to corrupted flags bytes
      fail();
    } catch (final SketchesArgumentException e) { }
  }

  @Test
  public void checkDirectUnionSingleItem2() {
    ThetaSketch sk = ThetaSketch.wrap(siSkWoutSiFlag24Bytes());
    assertEquals(sk.getEstimate(), 1.0, 0.0);
    //println(sk.toString());
    sk = ThetaSketch.wrap(siSkWithSiFlag24Bytes());
    assertEquals(sk.getEstimate(), 1.0, 0.0);
    //println(sk.toString());
  }

  @Test
  public void checkSingleItemCompact() {
    final UpdatableThetaSketch sk1 = new UpdatableThetaSketchBuilder().build();
    sk1.update(1);
    final CompactThetaSketch csk = sk1.compact();
    assertTrue(csk instanceof SingleItemSketch);
    final CompactThetaSketch csk2 = csk.compact();
    assertEquals(csk, csk2);
    final CompactThetaSketch csk3 = csk.compact(true, MemorySegment.ofArray(new byte[16]));
    assertTrue(csk3 instanceof DirectCompactSketch);
    assertEquals(csk2.getCurrentPreambleLongs(), 1);
    assertEquals(csk3.getCurrentPreambleLongs(), 1);
  }


  static final long SiSkPre0WithSiFlag = 0x93cc3a0000030301L;
  static final long SiSkPre0WoutSiFlag = 0x93cc1a0000030301L;
  static final long Hash = 0x05a186bdcb7df915L;

  static MemorySegment siSkWithSiFlag24Bytes() {
    final int cap = 24; //8 extra bytes
    final MemorySegment wseg  = MemorySegment.ofArray(new byte[cap]);
    wseg .set(JAVA_LONG_UNALIGNED, 0, SiSkPre0WithSiFlag);
    wseg .set(JAVA_LONG_UNALIGNED, 8, Hash);
    return wseg ;
  }

  static MemorySegment siSkWoutSiFlag24Bytes() {
    final int cap = 24; //8 extra bytes
    final MemorySegment wseg  = MemorySegment.ofArray(new byte[cap]);
    wseg .set(JAVA_LONG_UNALIGNED, 0, SiSkPre0WoutSiFlag);
    wseg .set(JAVA_LONG_UNALIGNED, 8, Hash);
    return wseg;
  }

  @Test
  public void printlnTest() {
    println("PRINTING: "+this.getClass().getName());
  }

  /**
   * @param s value to print
   */
  static void println(final String s) {
    //System.out.println(s); //disable here
  }

}
