/*
 * Copyright DataStax, Inc.
 *
 * Licensed 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:
                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:
                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:
                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:
                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:
                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);
    }

}
