/*
 * 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_LONG_UNALIGNED;
import static java.nio.charset.StandardCharsets.UTF_8;
import static org.apache.datasketches.common.ByteArrayUtil.putLongLE;
import static org.apache.datasketches.hash.MurmurHash3.hash;
import static org.apache.datasketches.theta.PreambleUtil.SINGLEITEM_FLAG_MASK;
import static org.apache.datasketches.theta.PreambleUtil.checkSegPreambleCap;
import static org.apache.datasketches.theta.PreambleUtil.extractFamilyID;
import static org.apache.datasketches.theta.PreambleUtil.extractFlags;
import static org.apache.datasketches.theta.PreambleUtil.extractSeedHash;
import static org.apache.datasketches.theta.PreambleUtil.extractSerVer;

import java.lang.foreign.MemorySegment;

import org.apache.datasketches.common.Family;
import org.apache.datasketches.common.SketchesArgumentException;
import org.apache.datasketches.common.Util;

/**
 * A CompactThetaSketch that holds only one item hash.
 *
 * @author Lee Rhodes
 */
final class SingleItemSketch extends CompactThetaSketch {
  private static final long DEFAULT_SEED_HASH = Util.computeSeedHash(Util.DEFAULT_UPDATE_SEED) & 0xFFFFL;

  // For backward compatibility, a candidate pre0_ long must have:
  // Flags (byte 5): Ordered, Compact, NOT Empty, Read Only, NOT BigEndian = 11010 = 0x1A. (without SI flag)
  // Flags mask will be 0x1F.
  // SingleItem flag may not be set due to a historical bug, so we can't depend on it for now.
  // However, if the above flags are correct, preLongs == 1, SerVer >= 3, FamilyID == 3,
  // and the hash seed matches, it is virtually guaranteed that we have a SingleItemThetaSketch.

  private static final long PRE0_LO6_SI   = 0X00_00_3A_00_00_03_03_01L; //low 6 bytes, with SI flag
  private long pre0_ = 0;
  private long hash_ = 0;

  //Internal Constructor. All checking & hashing has been done, assumes default seed
  private SingleItemSketch(final long hash) {
    pre0_ = (DEFAULT_SEED_HASH << 48) | PRE0_LO6_SI;
    hash_ = hash;
  }

  //All checking & hashing has been done, given the relevant seed
  SingleItemSketch(final long hash, final long seed) {
    final long seedHash = Util.computeSeedHash(seed) & 0xFFFFL;
    pre0_ = (seedHash << 48) | PRE0_LO6_SI;
    hash_ = hash;
  }

  //All checking & hashing has been done, given the relevant seedHash
  SingleItemSketch(final long hash, final short seedHash) {
    final long seedH = seedHash & 0xFFFFL;
    pre0_ = (seedH << 48) | PRE0_LO6_SI;
    hash_ = hash;
  }

  /**
   * Creates a SingleItemSketch on the heap given a SingleItemThetaSketch MemorySegment image and a seedHash.
   * Checks the seed hash of the given MemorySegment against the given seedHash.
   * @param srcSeg the MemorySegment to be heapified.
   * @param expectedSeedHash the given seedHash to be checked against the srcSeg seedHash
   * @return a SingleItemThetaSketch
   */ //does not override ThetaSketch
  static SingleItemSketch heapify(final MemorySegment srcSeg, final short expectedSeedHash) {
    Util.checkSeedHashes((short) extractSeedHash(srcSeg), expectedSeedHash);
    final boolean singleItem = checkForSingleItem(srcSeg);
    if (singleItem) { return new SingleItemSketch(srcSeg.get(JAVA_LONG_UNALIGNED, 8), expectedSeedHash); }
    throw new SketchesArgumentException("Input MemorySegment is not a SingleItemSketch.");
  }

  @Override
  public CompactThetaSketch compact(final boolean dstOrdered, final MemorySegment dstSeg) {
    if (dstSeg == null) { return this; }
    else {
      dstSeg.set(JAVA_LONG_UNALIGNED, 0, pre0_);
      dstSeg.set(JAVA_LONG_UNALIGNED, 8, hash_);
      return new DirectCompactSketch(dstSeg);
    }
  }

  //Create methods using the default seed

  /**
   * Create this sketch with a long.
   *
   * @param datum The given long datum.
   * @return a SingleItemThetaSketch
   */
  static SingleItemSketch create(final long datum) {
    final long[] data = { datum };
    return new SingleItemSketch(hash(data, Util.DEFAULT_UPDATE_SEED)[0] >>> 1);
  }

  /**
   * Create this sketch with the given double (or float) datum.
   * The double will be converted to a long using Double.doubleToLongBits(datum),
   * which normalizes all NaN values to a single NaN representation.
   * Plus and minus zero will be normalized to plus zero.
   * The special floating-point values NaN and +/- Infinity are treated as distinct.
   *
   * @param datum The given double datum.
   * @return a SingleItemThetaSketch
   */
  static SingleItemSketch create(final double datum) {
    final double d = (datum == 0.0) ? 0.0 : datum; // canonicalize -0.0, 0.0
    final long[] data = { Double.doubleToLongBits(d) };// canonicalize all NaN forms
    return new SingleItemSketch(hash(data, Util.DEFAULT_UPDATE_SEED)[0] >>> 1);
  }

  /**
   * Create this sketch with the given String.
   * The string is converted to a byte array using UTF8 encoding.
   * If the string is null or empty no create attempt is made and the method returns null.
   *
   * <p>Note: this will not produce the same hash values as the {@link #create(char[])}
   * method and will generally be a little slower depending on the complexity of the UTF8 encoding.
   * </p>
   *
   * @param datum The given String.
   * @return a SingleItemThetaSketch or null
   */
  static SingleItemSketch create(final String datum) {
    if ((datum == null) || datum.isEmpty()) { return null; }
    final byte[] data = datum.getBytes(UTF_8);
    return new SingleItemSketch(hash(data, Util.DEFAULT_UPDATE_SEED)[0] >>> 1);
  }

  /**
   * Create this sketch with the given byte array.
   * If the byte array is null or empty no create attempt is made and the method returns null.
   *
   * @param data The given byte array.
   * @return a SingleItemThetaSketch or null
   */
  static SingleItemSketch create(final byte[] data) {
    if ((data == null) || (data.length == 0)) { return null; }
    return new SingleItemSketch(hash(data, Util.DEFAULT_UPDATE_SEED)[0] >>> 1);
  }

  /**
   * Create this sketch with the given char array.
   * If the char array is null or empty no create attempt is made and the method returns null.
   *
   * <p>Note: this will not produce the same output hash values as the {@link #create(String)}
   * method but will be a little faster as it avoids the complexity of the UTF8 encoding.</p>
   *
   * @param data The given char array.
   * @return a SingleItemThetaSketch or null
   */
  static SingleItemSketch create(final char[] data) {
    if ((data == null) || (data.length == 0)) { return null; }
    return new SingleItemSketch(hash(data, Util.DEFAULT_UPDATE_SEED)[0] >>> 1);
  }

  /**
   * Create this sketch with the given integer array.
   * If the integer array is null or empty no create attempt is made and the method returns null.
   *
   * @param data The given int array.
   * @return a SingleItemThetaSketch or null
   */
  static SingleItemSketch create(final int[] data) {
    if ((data == null) || (data.length == 0)) { return null; }
    return new SingleItemSketch(hash(data, Util.DEFAULT_UPDATE_SEED)[0] >>> 1);
  }

  /**
   * Create this sketch with the given long array.
   * If the long array is null or empty no create attempt is made and the method returns null.
   *
   * @param data The given long array.
   * @return a SingleItemThetaSketch or null
   */
  static SingleItemSketch create(final long[] data) {
    if ((data == null) || (data.length == 0)) { return null; }
    return new SingleItemSketch(hash(data, Util.DEFAULT_UPDATE_SEED)[0] >>> 1);
  }

  //Updates with a user specified seed

  /**
   * Create this sketch with a long and a seed.
   *
   * @param datum The given long datum.
   * @param seed used to hash the given value.
   * @return a SingleItemThetaSketch
   */
  static SingleItemSketch create(final long datum, final long seed) {
    final long[] data = { datum };
    return new SingleItemSketch(hash(data, seed)[0] >>> 1);
  }

  /**
   * Create this sketch with the given double (or float) datum and a seed.
   * The double will be converted to a long using Double.doubleToLongBits(datum),
   * which normalizes all NaN values to a single NaN representation.
   * Plus and minus zero will be normalized to plus zero.
   * The special floating-point values NaN and +/- Infinity are treated as distinct.
   *
   * @param datum The given double datum.
   * @param seed used to hash the given value.
   * @return a SingleItemThetaSketch
   */
  static SingleItemSketch create(final double datum, final long seed) {
    final double d = (datum == 0.0) ? 0.0 : datum; // canonicalize -0.0, 0.0
    final long[] data = { Double.doubleToLongBits(d) };// canonicalize all NaN forms
    return new SingleItemSketch(hash(data, seed)[0] >>> 1, seed);
  }

  /**
   * Create this sketch with the given String and a seed.
   * The string is converted to a byte array using UTF8 encoding.
   * If the string is null or empty no create attempt is made and the method returns null.
   *
   * <p>Note: this will not produce the same output hash values as the {@link #create(char[])}
   * method and will generally be a little slower depending on the complexity of the UTF8 encoding.
   * </p>
   *
   * @param datum The given String.
   * @param seed used to hash the given value.
   * @return a SingleItemThetaSketch or null
   */
  static SingleItemSketch create(final String datum, final long seed) {
    if ((datum == null) || datum.isEmpty()) { return null; }
    final byte[] data = datum.getBytes(UTF_8);
    return new SingleItemSketch(hash(data, seed)[0] >>> 1, seed);
  }

  /**
   * Create this sketch with the given byte array and a seed.
   * If the byte array is null or empty no create attempt is made and the method returns null.
   *
   * @param data The given byte array.
   * @param seed used to hash the given value.
   * @return a SingleItemThetaSketch or null
   */
  static SingleItemSketch create(final byte[] data, final long seed) {
    if ((data == null) || (data.length == 0)) { return null; }
    return new SingleItemSketch(hash(data, seed)[0] >>> 1, seed);
  }

  /**
   * Create this sketch with the given char array and a seed.
   * If the char array is null or empty no create attempt is made and the method returns null.
   *
   * <p>Note: this will not produce the same output hash values as the {@link #create(String)}
   * method but will be a little faster as it avoids the complexity of the UTF8 encoding.</p>
   *
   * @param data The given char array.
   * @param seed used to hash the given value.
   * @return a SingleItemThetaSketch or null
   */
  static SingleItemSketch create(final char[] data, final long seed) {
    if ((data == null) || (data.length == 0)) { return null; }
    return new SingleItemSketch(hash(data, seed)[0] >>> 1, seed);
  }

  /**
   * Create this sketch with the given integer array and a seed.
   * If the integer array is null or empty no create attempt is made and the method returns null.
   *
   * @param data The given int array.
   * @param seed used to hash the given value.
   * @return a SingleItemThetaSketch or null
   */
  static SingleItemSketch create(final int[] data, final long seed) {
    if ((data == null) || (data.length == 0)) { return null; }
    return new SingleItemSketch(hash(data, seed)[0] >>> 1, seed);
  }

  /**
   * Create this sketch with the given long array (as an item) and a seed.
   * If the long array is null or empty no create attempt is made and the method returns null.
   *
   * @param data The given long array.
   * @param seed used to hash the given value.
   * @return a SingleItemThetaSketch or null
   */
  static SingleItemSketch create(final long[] data, final long seed) {
    if ((data == null) || (data.length == 0)) { return null; }
    return new SingleItemSketch(hash(data, seed)[0] >>> 1, seed);
  }

  //ThetaSketch

  @Override //much faster
  public int getCountLessThanThetaLong(final long thetaLong) {
    return (hash_ < thetaLong) ? 1 : 0;
  }

  @Override
  public int getCurrentBytes() {
    return 16;
  }

  @Override
  public double getEstimate() {
    return 1.0;
  }

  @Override
  public HashIterator iterator() {
    return new HeapCompactHashIterator(new long[] { hash_ });
  }

  @Override
  public double getLowerBound(final int numStdDev) {
    return 1.0;
  }

  @Override
  public int getRetainedEntries(final boolean valid) { //valid is only relevant for the AlphaSketch
    return 1;
  }

  @Override
  public long getThetaLong() {
    return Long.MAX_VALUE;
  }

  @Override
  public double getUpperBound(final int numStdDev) {
    return 1.0;
  }

  @Override
  public boolean isEmpty() {
    return false;
  }

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

  @Override
  public byte[] toByteArray() {
    final byte[] out = new byte[16];
    putLongLE(out, 0, pre0_);
    putLongLE(out, 8, hash_);
    return out;
  }

  //restricted methods

  @Override
  long[] getCache() {
    return new long[] { hash_ };
  }

  @Override
  int getCompactPreambleLongs() {
    return 1;
  }

  @Override
  int getCurrentPreambleLongs() {
    return 1;
  }

  @Override
  short getSeedHash() {
    return (short) (pre0_ >>> 48);
  }

  static boolean checkForSingleItem(final MemorySegment seg) {
    final int preLongs = checkSegPreambleCap(seg);
    return checkForSingleItem(preLongs, extractSerVer(seg), extractFamilyID(seg), extractFlags(seg) );
  }

  static boolean checkForSingleItem(
      final int preLongs,
      final int serVer,
      final int famId,
      final int flags) {
    // Flags byte: SI=X, Ordered=T, Compact=T, Empty=F, ReadOnly=T, Reserved(BE)=F = X11010 = 0x1A.
    // Flags mask will be 0x1F.
    // SingleItem flag may not be set due to a historical bug, so we can't depend on it for now.
    // However, if the above flags are correct, preLongs == 1, SerVer >= 3, FamilyID == 3,
    // and the hash seed matches (not done here), it is virtually guaranteed that we have a
    // SingleItemThetaSketch.
    final boolean preLongsOK = preLongs == 1;
    final boolean serVerOK = serVer >= 3;
    final boolean famIdOK = famId == Family.COMPACT.getID();
    final boolean flagsOK =  (flags & 0x1F) == 0x1A; //no SI, yet
    final boolean singleFlagOK = (flags & SINGLEITEM_FLAG_MASK) > 0;
    return (preLongsOK && serVerOK && famIdOK && flagsOK) || singleFlagOK;
  }

}
