package org.roaringbitmap.longlong;

import static java.nio.ByteOrder.LITTLE_ENDIAN;

import java.io.DataInput;
import java.io.DataOutput;
import java.io.IOException;
import java.nio.ByteBuffer;
import java.util.NoSuchElementException;

import org.roaringbitmap.Container;
import org.roaringbitmap.art.*;

public class HighLowContainer {

  private Art art;
  private Containers containers;
  private static final byte EMPTY_TAG = 0;
  private static final byte NOT_EMPTY_TAG = 1;

  public HighLowContainer() {
    art = new Art();
    containers = new Containers();
  }

  public Container getContainer(long containerIdx) {
    return containers.getContainer(containerIdx);
  }

  /**
   * search the container by the given 48 bit high part key
   * @param highPart the 48 bit key array
   * @return the container with the container index
   */
  public ContainerWithIndex searchContainer(byte[] highPart) {
    long containerIdx = art.findByKey(highPart);
    if (containerIdx < 0) {
      return null;
    } else {
      Container container = containers.getContainer(containerIdx);
      return new ContainerWithIndex(container, containerIdx);
    }
  }

  /**
   * put the 48 bit key and the corresponding container
   * @param highPart the 48 bit key
   * @param container the container
   */
  public void put(byte[] highPart, Container container) {
    long containerIdx = containers.addContainer(container);
    art.insert(highPart, containerIdx);
  }

  /**
   * Attempt to remove the container that corresponds to the 48 bit key.
   * @param highPart the 48 bit key
   */
  public void remove(byte[] highPart) {
    long containerIdx = art.remove(highPart);
    if (containerIdx != Node.ILLEGAL_IDX) {
      containers.remove(containerIdx);
    }
  }

  /**
   * get a container iterator
   * @return a container iterator
   */
  public ContainerIterator containerIterator() {
    return containers.iterator();
  }

  /**
   * get a key iterator
   * @return a key iterator
   */
  public KeyIterator highKeyIterator() {
    return art.iterator(containers);
  }

  /**
   * @param reverse true ascending order, false: descending order
   * @return the leaf node iterator
   */
  public LeafNodeIterator highKeyLeafNodeIterator(boolean reverse) {
    return art.leafNodeIterator(reverse, containers);
  }

  public LeafNodeIterator highKeyLeafNodeIteratorFrom(long bound, boolean reverse) {
    return art.leafNodeIteratorFrom(bound, reverse, containers);
  }

  /**
   * replace the specified position one with a fresh container
   * @param containerIdx the position of the container
   * @param container the fresh container
   */
  public void replaceContainer(long containerIdx, Container container) {
    containers.replace(containerIdx, container);
  }

  /**
   * whether it's empty
   * @return true: empty,false: not empty
   */
  public boolean isEmpty() {
    return art.isEmpty();
  }

  private void assertNonEmpty() {
    if(isEmpty()) {
      throw new NoSuchElementException("Empty " + this.getClass().getSimpleName());
    }
  }

  /**
   * Gets the first value in the array
   * @return the first value in the array
   * @throws NoSuchElementException if empty
   */
  public long first() {
    assertNonEmpty();

    LeafNode firstNode = art.first();
    long containerIdx = firstNode.getContainerIdx();
    Container container = getContainer(containerIdx);
    byte[] high = firstNode.getKeyBytes();
    char low = (char) container.first();
    return LongUtils.toLong(high, low);
  }


  /**
   * Gets the last value in the array
   * @return the last value in the array
   * @throws NoSuchElementException if empty
   */
  public long last() {
    assertNonEmpty();

    LeafNode lastNode = art.last();
    long containerIdx = lastNode.getContainerIdx();
    Container container = getContainer(containerIdx);
    byte[] high = lastNode.getKeyBytes();
    char low = (char) container.last();
    return LongUtils.toLong(high, low);
  }

  /**
   * serialize into the ByteBuffer in little endian
   * @param buffer the ByteBuffer should be large enough to hold the data
   * @throws IOException indicate exception happened
   */
  public void serialize(ByteBuffer buffer) throws IOException {
    ByteBuffer byteBuffer = buffer.order() == LITTLE_ENDIAN ? buffer
        : buffer.slice().order(LITTLE_ENDIAN);
    if (art.isEmpty()) {
      byteBuffer.put(EMPTY_TAG);
      return;
    } else {
      byteBuffer.put(NOT_EMPTY_TAG);
    }
    art.serializeArt(byteBuffer);
    containers.serialize(byteBuffer);
    if (byteBuffer != buffer) {
      buffer.position(buffer.position() + byteBuffer.position());
    }
  }

  /**
   * deserialize from the input ByteBuffer in little endian
   * @param buffer the ByteBuffer
   * @throws IOException indicate exception happened
   */
  public void deserialize(ByteBuffer buffer) throws IOException {
    ByteBuffer byteBuffer = buffer.order() == LITTLE_ENDIAN ? buffer
        : buffer.slice().order(LITTLE_ENDIAN);
    clear();
    byte emptyTag = byteBuffer.get();
    if (emptyTag == EMPTY_TAG) {
      return;
    }
    art.deserializeArt(byteBuffer);
    containers.deserialize(byteBuffer);
  }

  /**
   * serialized size in bytes
   * @return the size in bytes
   */
  public long serializedSizeInBytes() {
    long totalSize = 1L;
    if (art.isEmpty()) {
      return totalSize;
    }
    totalSize += art.serializeSizeInBytes();
    totalSize += containers.serializedSizeInBytes();
    return totalSize;
  }

  /**
   * serialize into the byte stream
   * @param dataOutput the output stream
   * @throws IOException indicate the io exception happened
   */
  public void serialize(DataOutput dataOutput) throws IOException {
    if (art.isEmpty()) {
      dataOutput.writeByte(EMPTY_TAG);
      return;
    } else {
      dataOutput.writeByte(NOT_EMPTY_TAG);
    }
    art.serializeArt(dataOutput);
    containers.serialize(dataOutput);
  }

  /**
   * deserialize from the input byte stream
   * @param dataInput the input byte stream
   * @throws IOException indicate the io exception happened
   */
  public void deserialize(DataInput dataInput) throws IOException {
    clear();
    byte emptyTag = dataInput.readByte();
    if (emptyTag == EMPTY_TAG) {
      return;
    }
    art.deserializeArt(dataInput);
    containers.deserialize(dataInput);
  }

  /**
   * clear to be a empty fresh one
   */
  public void clear() {
    art = new Art();
    containers = new Containers();
  }

  @Override
  public int hashCode() {
    int hashCode = 0;
    KeyIterator keyIterator = highKeyIterator();
    while (keyIterator.hasNext()) {
      byte[] key = keyIterator.next();
      int result = 1;
      for (byte element : key) {
        result = 31 * result + element;
      }
      long containerIdx = keyIterator.currentContainerIdx();
      Container container = containers.getContainer(containerIdx);
      hashCode = 31 * hashCode + result + container.hashCode();
    }
    return hashCode;
  }

  @Override
  public boolean equals(Object object) {
    if (object instanceof HighLowContainer) {
      HighLowContainer otherHighLowContainer = (HighLowContainer) object;
      if (this.art.getKeySize() != otherHighLowContainer.art.getKeySize()) {
        return false;
      }
      KeyIterator thisKeyIte = this.highKeyIterator();
      while (thisKeyIte.hasNext()) {
        byte[] thisHigh = thisKeyIte.next();
        long containerIdx = thisKeyIte.currentContainerIdx();
        Container thisContainer = this.getContainer(containerIdx);
        ContainerWithIndex containerWithIndex = otherHighLowContainer.searchContainer(thisHigh);
        if (containerWithIndex == null) {
          return false;
        }
        Container otherContainer = containerWithIndex.getContainer();
        if (!thisContainer.equals(otherContainer)) {
          return false;
        }
      }
      return true;
    }
    return false;
  }
}
