content: /*************************************************************************
  *                                                                        *
  *  This file is part of the 20n/act project.                             *
  *  20n/act enables DNA prediction for synthetic biology/bioengineering.  *
  *  Copyright (C) 2017 20n Labs, Inc.                                     *
  *                                                                        *
  *  Please direct all queries to act@20n.com.                             *
  *                                                                        *
  *  This program is free software: you can redistribute it and/or modify  *
  *  it under the terms of the GNU General Public License as published by  *
  *  the Free Software Foundation, either version 3 of the License, or     *
  *  (at your option) any later version.                                   *
  *                                                                        *
  *  This program is distributed in the hope that it will be useful,       *
  *  but WITHOUT ANY WARRANTY; without even the implied warranty of        *
  *  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the         *
  *  GNU General Public License for more details.                          *
  *                                                                        *
  *  You should have received a copy of the GNU General Public License     *
  *  along with this program.  If not, see <http://www.gnu.org/licenses/>. *
  *                                                                        *
  *************************************************************************/
  
  package com.act.lcms.v2.fullindex;
  
  import java.io.ByteArrayInputStream;
  import java.io.ByteArrayOutputStream;
  import java.io.IOException;
  import java.io.ObjectInputStream;
  import java.io.ObjectOutputStream;
  import java.nio.ByteBuffer;
  import java.util.ArrayList;
  import java.util.List;
  
  /**
   * Package-private utility methods for de/serialization and byte buffer manipulation.
   */
  public class Utils {
  
    /**
     * Seralize a Serializable object.
     * @param obj The object to serialize.
     * @param <T> The type of the object to serialize.  Note that this is not bound by serializable for convenience.
     * @return The binary representation of that object.
     * @throws IOException
     */
    static <T> byte[] serializeObject(T obj) throws IOException {
      try (ByteArrayOutputStream bos = new ByteArrayOutputStream();
           ObjectOutputStream oo = new ObjectOutputStream(bos)) {
        oo.writeObject(obj);
        oo.flush();
        return bos.toByteArray();
      }
    }
  
    /**
     * Deserialize an object that had been serialized via {@link #serializeObject(Object)}.
     * @param bytes The binary representation of an object.
     * @param <T> The type of the object to deserialize.
     * @return The deserialized object.
     * @throws IOException
     * @throws ClassNotFoundException
     */
    static <T> T deserializeObject(byte[] bytes) throws IOException, ClassNotFoundException {
      try (ObjectInputStream ois = new ObjectInputStream(new ByteArrayInputStream(bytes))) {
        // Assumes you know what you're getting into when deserializing.  Don't use this blindly.
        return (T) ois.readObject();
      }
    }
  
    /* ----------------------------------------
     * Byte buffer utilities.
     * These are all package private so that sibling classes can read from the DB.
     * */
  
    /**
     * Ensure that a buffer is prepared for reading from the beginning.
     * @param b The buffer to check.
     */
    static void assertReadyForFreshRead(ByteBuffer b) {
      assert(b.limit() != 0); // A zero limit would cause an immediate buffer underflow.
      assert(b.position() == 0); // A non-zero position means either the buffer hasn't been flipped or has been read from.
    }
  
    /**
     * Convert a list of floating point numbers to a (compact) array of bytes.  Note that this does not use object
     * output streams, but writes the floats' binary representations directly.
     * @param vals The values to serialize as raw bytes.
     * @return A byte array containing the floats in binary form.
     */
    static byte[] floatListToByteArray(List<Float> vals) {
      ByteBuffer buffer = ByteBuffer.allocate(vals.size() * Float.BYTES); // Don't waste any bits!
      vals.forEach(buffer::putFloat);
      buffer.flip(); // Prep for reading.
      return toCompactArray(buffer); // Compact just to be safe, check invariants.
    }
  
    /**
     * Convert an array of float bytes from {@link #floatListToByteArray(List)} back to a list of floats.
     * @param bytes The bytes to read.
     * @return A list of Float objects constructed from those bytes.
     */
    static List<Float> byteArrayToFloatList(byte[] bytes) {
      ByteBuffer buffer = ByteBuffer.wrap(bytes);
      List<Float> vals = new ArrayList<>(bytes.length / Float.BYTES);
      while (buffer.hasRemaining()) {
        vals.add(buffer.getFloat());
      }
      return vals;
    }
  
    /**
     * Return an appropriately sized byte array filled with the contents of src.
     *
     * The {@link ByteBuffer#array()} call returns the array that backs the buffer, which is sometimes larger than the
     * data contained in the buffer.  For instance, in this example:
     * <pre>
     * {@code
     *   ByteBuffer buffer = ByteBuffer.allocate(16);
     *   buffer.put("hello world!".getBytes());
     *   byte[] arr = buffer.array();
     * }
     * </pre>
     * {@code arr} will always have length 16, even though we've only written 12 bytes to it.
     *
     * @param src
     * @return
     */
    static byte[] toCompactArray(ByteBuffer src) {
      // Assume src is pre-flipped to avoid a double-flip.
      if (src.hasArray() && src.limit() >= src.capacity()) { // No need to compact if limit covers the full backing array.
        return src.array();
      }
      assertReadyForFreshRead(src);
  
      // Otherwise, we need to allocate and copy into a new array of the correct size.
  
      // TODO: is Arrays.copyOf any faster than this?  Ideally we want CoW semantics here...
      // Warning: do not use src.compact().  That does something entirely different.
  
      /* We use a byte array here rather than another byte buffer for two reasons:
       * 1) It's possible for byte buffers to reside off-heap (like in shared pages), and we definitely want the array on
       *    the heap.
       * 2) If the byte-buffer resides on the heap, it's allocation will be identical, and we save the object management
       *    overhead by not creating another ByteBuffer. */
      byte[] dest = new byte[src.limit()];
      src.get(dest); // Copy the contents of the buffer into our appropriately sized array.
      assert(src.position() == src.limit()); // All bytes should have been read.
      src.rewind(); // Be kind: rewind.
      return dest;
    }
  
    /**
     * Appends the contents of toAppend to dest if there is capacity, or does the equivalent of C's "realloc" by
     * allocating a larger buffer and copying both dest and toAppend into that new buffer.
     *
     * Always call this function as:
     * <pre>
     * {@code
     *   dest = appendOrRealloc(dest, toAppend)
     * }
     * </pre>
     * as we can't actually resize or reallocate dest once it's been allocated.
     *
     * @param dest The buffer into which to write or to reallocate if necessary.
     * @param toAppend The data to write into dest.
     * @return dest or a new, larger buffer containing both dest and toAppend.
     */
    static ByteBuffer appendOrRealloc(ByteBuffer dest, ByteBuffer toAppend) {
      /* Before reading this function, review this excellent explanation of the behaviors of ByteBuffers:
       * http://mindprod.com/jgloss/bytebuffer.html */
  
      // Assume toAppend is pre-flipped to avoid a double flip, which would be Very Bad.
      if (dest.capacity() - dest.position() < toAppend.limit()) {
        // Whoops, we have to reallocate!
  
        ByteBuffer newDest = ByteBuffer.allocate(dest.capacity() << 1); // TODO: make sure these are page-divisible sizes.
        dest.flip(); // Switch dest to reading mode.
        newDest.put(dest); // Write all of dest into the new buffer.
        dest = newDest;
      }
      dest.put(toAppend);
      return dest;
    }
  }
  