/*
 * 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 com.datastax.driver.core;

import java.nio.ByteBuffer;

/** A set of utility methods to deal with type conversion and serialization. */
public final class CodecUtils {

  private static final long MAX_CQL_LONG_VALUE = ((1L << 32) - 1);

  private static final long EPOCH_AS_CQL_LONG = (1L << 31);

  private CodecUtils() {}

  /**
   * Utility method that "packs" together a list of {@link ByteBuffer}s containing serialized
   * collection elements. Mainly intended for use with collection codecs when serializing
   * collections.
   *
   * @param buffers the collection elements
   * @param elements the total number of elements
   * @param version the protocol version to use
   * @return The serialized collection
   */
  public static ByteBuffer pack(ByteBuffer[] buffers, int elements, ProtocolVersion version) {
    int size = 0;
    for (ByteBuffer bb : buffers) {
      int elemSize = sizeOfValue(bb, version);
      size += elemSize;
    }
    ByteBuffer result = ByteBuffer.allocate(sizeOfCollectionSize(version) + size);
    writeSize(result, elements, version);
    for (ByteBuffer bb : buffers) writeValue(result, bb, version);
    return (ByteBuffer) result.flip();
  }

  /**
   * Utility method that reads a size value. Mainly intended for collection codecs when
   * deserializing CQL collections.
   *
   * @param input The ByteBuffer to read from.
   * @param version The protocol version to use.
   * @return The size value.
   */
  public static int readSize(ByteBuffer input, ProtocolVersion version) {
    switch (version) {
      case V1:
      case V2:
        return getUnsignedShort(input);
      case V3:
      case V4:
      case V5:
      case V6:
        return input.getInt();
      default:
        throw version.unsupported();
    }
  }

  /**
   * Utility method that writes a size value. Mainly intended for collection codecs when serializing
   * CQL collections.
   *
   * @param output The ByteBuffer to write to.
   * @param size The collection size.
   * @param version The protocol version to use.
   */
  public static void writeSize(ByteBuffer output, int size, ProtocolVersion version) {
    switch (version) {
      case V1:
      case V2:
        if (size > 65535)
          throw new IllegalArgumentException(
              String.format(
                  "Native protocol version %d supports up to 65535 elements in any collection - but collection contains %d elements",
                  version.toInt(), size));
        output.putShort((short) size);
        break;
      case V3:
      case V4:
      case V5:
      case V6:
        output.putInt(size);
        break;
      default:
        throw version.unsupported();
    }
  }

  /**
   * Utility method that reads a value. Mainly intended for collection codecs when deserializing CQL
   * collections.
   *
   * @param input The ByteBuffer to read from.
   * @param version The protocol version to use.
   * @return The collection element.
   */
  public static ByteBuffer readValue(ByteBuffer input, ProtocolVersion version) {
    int size = readSize(input, version);
    return size < 0 ? null : readBytes(input, size);
  }

  /**
   * Utility method that writes a value. Mainly intended for collection codecs when deserializing
   * CQL collections.
   *
   * @param output The ByteBuffer to write to.
   * @param value The value to write.
   * @param version The protocol version to use.
   */
  public static void writeValue(ByteBuffer output, ByteBuffer value, ProtocolVersion version) {
    switch (version) {
      case V1:
      case V2:
        assert value != null;
        output.putShort((short) value.remaining());
        output.put(value.duplicate());
        break;
      case V3:
      case V4:
      case V5:
      case V6:
        if (value == null) {
          output.putInt(-1);
        } else {
          output.putInt(value.remaining());
          output.put(value.duplicate());
        }
        break;
      default:
        throw version.unsupported();
    }
  }

  /**
   * Read {@code length} bytes from {@code bb} into a new ByteBuffer.
   *
   * @param bb The ByteBuffer to read.
   * @param length The number of bytes to read.
   * @return The read bytes.
   */
  public static ByteBuffer readBytes(ByteBuffer bb, int length) {
    ByteBuffer copy = bb.duplicate();
    copy.limit(copy.position() + length);
    bb.position(bb.position() + length);
    return copy;
  }

  /**
   * Converts an "unsigned" int read from a DATE value into a signed int.
   *
   * <p>The protocol encodes DATE values as <em>unsigned</em> ints with the Epoch in the middle of
   * the range (2^31). This method handles the conversion from an "unsigned" to a signed int.
   */
  public static int fromUnsignedToSignedInt(int unsigned) {
    return unsigned + Integer.MIN_VALUE; // this relies on overflow for "negative" values
  }

  /**
   * Converts an int into an "unsigned" int suitable to be written as a DATE value.
   *
   * <p>The protocol encodes DATE values as <em>unsigned</em> ints with the Epoch in the middle of
   * the range (2^31). This method handles the conversion from a signed to an "unsigned" int.
   */
  public static int fromSignedToUnsignedInt(int signed) {
    return signed - Integer.MIN_VALUE;
  }

  /**
   * Convert from a raw CQL long representing a numeric DATE literal to the number of days since the
   * Epoch. In CQL, numeric DATE literals are longs (unsigned integers actually) between 0 and 2^32
   * - 1, with the epoch in the middle; this method re-centers the epoch at 0.
   *
   * @param raw The CQL date value to convert.
   * @return The number of days since the Epoch corresponding to the given raw value.
   * @throws IllegalArgumentException if the value is out of range.
   */
  public static int fromCqlDateToDaysSinceEpoch(long raw) {
    if (raw < 0 || raw > MAX_CQL_LONG_VALUE)
      throw new IllegalArgumentException(
          String.format(
              "Numeric literals for DATE must be between 0 and %d (got %d)",
              MAX_CQL_LONG_VALUE, raw));
    return (int) (raw - EPOCH_AS_CQL_LONG);
  }

  /**
   * Convert the number of days since the Epoch into a raw CQL long representing a numeric DATE
   * literal.
   *
   * <p>In CQL, numeric DATE literals are longs (unsigned integers actually) between 0 and 2^32 - 1,
   * with the epoch in the middle; this method re-centers the epoch at 2^31.
   *
   * @param days The number of days since the Epoch convert.
   * @return The CQL date value corresponding to the given value.
   */
  public static long fromDaysSinceEpochToCqlDate(int days) {
    return ((long) days + EPOCH_AS_CQL_LONG);
  }

  private static int sizeOfCollectionSize(ProtocolVersion version) {
    switch (version) {
      case V1:
      case V2:
        return 2;
      case V3:
      case V4:
      case V5:
      case V6:
        return 4;
      default:
        throw version.unsupported();
    }
  }

  private static int sizeOfValue(ByteBuffer value, ProtocolVersion version) {
    switch (version) {
      case V1:
      case V2:
        int elemSize = value.remaining();
        if (elemSize > 65535)
          throw new IllegalArgumentException(
              String.format(
                  "Native protocol version %d supports only elements with size up to 65535 bytes - but element size is %d bytes",
                  version.toInt(), elemSize));
        return 2 + elemSize;
      case V3:
      case V4:
      case V5:
      case V6:
        return value == null ? 4 : 4 + value.remaining();
      default:
        throw version.unsupported();
    }
  }

  private static int getUnsignedShort(ByteBuffer bb) {
    int length = (bb.get() & 0xFF) << 8;
    return length | (bb.get() & 0xFF);
  }
}
