/*
 * 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.cassandra.db.marshal;

import java.io.IOException;
import java.nio.ByteBuffer;
import java.nio.FloatBuffer;
import java.nio.charset.CharacterCodingException;
import java.nio.charset.Charset;
import java.util.UUID;

import org.apache.cassandra.db.Digest;
import org.apache.cassandra.db.TypeSizes;
import org.apache.cassandra.io.util.DataInputPlus;
import org.apache.cassandra.io.util.DataOutputPlus;
import org.apache.cassandra.service.paxos.Ballot;
import org.apache.cassandra.utils.ByteBufferUtil;
import org.apache.cassandra.utils.FastByteOperations;
import org.apache.cassandra.utils.TimeUUID;
import org.apache.cassandra.utils.UUIDGen;

/**
 * Accessor for ByteBuffer values. ByteBufferAccessor treates {@link ByteBuffer#position()} as index 0,
 * and {@link ByteBuffer#remaining()} as the length.
 */
public class ByteBufferAccessor implements ValueAccessor<ByteBuffer>
{
    public static final ValueAccessor<ByteBuffer> instance = new ByteBufferAccessor();

    private ByteBufferAccessor() {}

    @Override
    public int size(ByteBuffer value)
    {
        return value.remaining();
    }

    @Override
    public ByteBuffer[] createArray(int length)
    {
        return new ByteBuffer[length];
    }

    @Override
    public void write(ByteBuffer value, DataOutputPlus out) throws IOException
    {
        out.write(value);
    }

    @Override
    public void write(ByteBuffer value, ByteBuffer out)
    {
        out.put(value.duplicate());
    }

    @Override
    public <V2> int copyTo(ByteBuffer src, int srcOffset, V2 dst, ValueAccessor<V2> dstAccessor, int dstOffset, int size)
    {
        dstAccessor.copyByteBufferTo(src, srcOffset, dst, dstOffset, size);
        return size;
    }

    @Override
    public int copyByteArrayTo(byte[] src, int srcOffset, ByteBuffer dst, int dstOffset, int size)
    {
        FastByteOperations.copy(src, srcOffset, dst, dst.position() + dstOffset, size);
        return size;
    }

    @Override
    public int copyByteBufferTo(ByteBuffer src, int srcOffset, ByteBuffer dst, int dstOffset, int size)
    {
        FastByteOperations.copy(src, src.position() + srcOffset, dst, dst.position() + dstOffset, size);
        return size;
    }

    @Override
    public void digest(ByteBuffer value, int offset, int size, Digest digest)
    {
        digest.update(value, value.position() + offset, size);
    }

    @Override
    public ByteBuffer read(DataInputPlus in, int length) throws IOException
    {
        return ByteBufferUtil.read(in, length);
    }

    @Override
    public ByteBuffer slice(ByteBuffer input, int offset, int length)
    {
        int size = sizeFromOffset(input, offset);
        if (size < length)
            throw new IndexOutOfBoundsException(String.format("Attempted to read %d, but the size is %d", length, size));
        ByteBuffer copy = input.duplicate();
        copy.position(copy.position() + offset);
        copy.limit(copy.position() + length);
        return copy;
    }

    @Override
    public <V2> int compare(ByteBuffer left, V2 right, ValueAccessor<V2> accessorR)
    {
        return accessorR.compareByteBufferTo(left, right);
    }

    @Override
    public int compareByteArrayTo(byte[] left, ByteBuffer right)
    {
        return ByteBufferUtil.compare(left, right);
    }

    @Override
    public int compareByteBufferTo(ByteBuffer left, ByteBuffer right)
    {
        return ByteBufferUtil.compareUnsigned(left, right);
    }

    @Override
    public ByteBuffer toBuffer(ByteBuffer value)
    {
        return value;
    }

    @Override
    public byte[] toArray(ByteBuffer value)
    {
        if (value == null)
            return null;
        return ByteBufferUtil.getArray(value);
    }

    @Override
    public byte[] toArray(ByteBuffer value, int offset, int length)
    {
        if (value == null)
            return null;
        return ByteBufferUtil.getArray(value, value.position() + offset, length);
    }

    @Override
    public String toString(ByteBuffer value, Charset charset) throws CharacterCodingException
    {
        return ByteBufferUtil.string(value, charset);
    }

    @Override
    public ByteBuffer valueOf(UUID v)
    {
        return UUIDGen.toByteBuffer(v);
    }

    @Override
    public String toHex(ByteBuffer value)
    {
        return ByteBufferUtil.bytesToHex(value);
    }

    @Override
    public byte toByte(ByteBuffer value)
    {
        return ByteBufferUtil.toByte(value);
    }

    @Override
    public byte getByte(ByteBuffer value, int offset)
    {
        return value.get(value.position() + offset);
    }

    @Override
    public short toShort(ByteBuffer value)
    {
        return ByteBufferUtil.toShort(value);
    }

    @Override
    public short getShort(ByteBuffer value, int offset)
    {
        return value.getShort(value.position() + offset);
    }

    @Override
    public int getUnsignedShort(ByteBuffer value, int offset)
    {
        return ByteBufferUtil.getUnsignedShort(value, value.position() + offset);
    }

    @Override
    public int toInt(ByteBuffer value)
    {
        return ByteBufferUtil.toInt(value);
    }

    @Override
    public int getInt(ByteBuffer value, int offset)
    {
        return value.getInt(value.position() + offset);
    }

    @Override
    public float getFloat(ByteBuffer value, int offset)
    {
        return value.getFloat(offset);
    }

    @Override
    public double getDouble(ByteBuffer value, int offset)
    {
        return value.getDouble(offset);
    }

    @Override
    public long toLong(ByteBuffer value)
    {
        return ByteBufferUtil.toLong(value);
    }

    @Override
    public long getLong(ByteBuffer value, int offset)
    {
        return value.getLong(value.position() + offset);
    }

    @Override
    public float toFloat(ByteBuffer value)
    {
        return ByteBufferUtil.toFloat(value);
    }

    @Override
    public float[] toFloatArray(ByteBuffer value, int dimension)
    {
        FloatBuffer floatBuffer = value.asFloatBuffer();
        if (floatBuffer.remaining() != dimension)
            throw new IllegalArgumentException(String.format("Could not convert to a float[] with different dimension. " +
                                                             "Was expecting %d but got %d", dimension, floatBuffer.remaining()));
        float[] floatArray = new float[floatBuffer.remaining()];
        floatBuffer.get(floatArray);
        return floatArray;
    }

    @Override
    public double toDouble(ByteBuffer value)
    {
        return ByteBufferUtil.toDouble(value);
    }

    @Override
    public UUID toUUID(ByteBuffer value)
    {
        return UUIDGen.getUUID(value);
    }

    @Override
    public TimeUUID toTimeUUID(ByteBuffer value)
    {
        return TimeUUID.fromBytes(value.getLong(value.position()), value.getLong(value.position() + 8));
    }

    @Override
    public Ballot toBallot(ByteBuffer value)
    {
        return Ballot.deserialize(value);
    }

    @Override
    public int putByte(ByteBuffer dst, int offset, byte value)
    {
        dst.put(dst.position() + offset, value);
        return TypeSizes.BYTE_SIZE;
    }

    @Override
    public int putShort(ByteBuffer dst, int offset, short value)
    {
        dst.putShort(dst.position() + offset, value);
        return TypeSizes.SHORT_SIZE;
    }

    @Override
    public int putInt(ByteBuffer dst, int offset, int value)
    {
        dst.putInt(dst.position() + offset, value);
        return TypeSizes.INT_SIZE;
    }

    @Override
    public int putLong(ByteBuffer dst, int offset, long value)
    {
        dst.putLong(dst.position() + offset, value);
        return TypeSizes.LONG_SIZE;
    }

    @Override
    public int putFloat(ByteBuffer dst, int offset, float value)
    {
        dst.putFloat(dst.position() + offset, value);
        return TypeSizes.FLOAT_SIZE;
    }

    @Override
    public int putLeastSignificantBytes(ByteBuffer dst, int offset, long register, int bytes)
    {
        int pos = dst.position() + offset;
        if (dst.limit() - pos < Long.BYTES)
        {
            return ValueAccessor.putLeastSignificantBytes(this, dst, offset, register, bytes);
        }
        else
        {
            dst.putLong(pos, register << (64 - (bytes * 8)));
        }
        return bytes;
    }

    @Override
    public long getLeastSignificantBytes(ByteBuffer dst, int offset, int bytes)
    {
        int pos = dst.position() + offset;
        if (dst.limit() - pos < Long.BYTES)
        {
            return ValueAccessor.getLeastSignificantBytes(this, dst, offset, bytes);
        }
        else
        {
            return dst.getLong(pos) >>> (64 - (bytes * 8));
        }
    }

    @Override
    public ByteBuffer empty()
    {
        return ByteBufferUtil.EMPTY_BYTE_BUFFER;
    }

    @Override
    public ByteBuffer valueOf(byte[] bytes)
    {
        return ByteBuffer.wrap(bytes);
    }

    @Override
    public ByteBuffer valueOf(ByteBuffer bytes)
    {
        return bytes;
    }

    @Override
    public ByteBuffer valueOf(String v, Charset charset)
    {
        return ByteBufferUtil.bytes(v, charset);
    }

    @Override
    public ByteBuffer valueOf(boolean v)
    {
        return v ? ByteBuffer.wrap(new byte[] {1}) : ByteBuffer.wrap(new byte[] {0});
    }

    @Override
    public ByteBuffer valueOf(byte v)
    {
        return ByteBufferUtil.bytes(v);
    }

    @Override
    public ByteBuffer valueOf(short v)
    {
        return ByteBufferUtil.bytes(v);
    }

    @Override
    public ByteBuffer valueOf(int v)
    {
        return ByteBufferUtil.bytes(v);
    }

    @Override
    public ByteBuffer valueOf(long v)
    {
        return ByteBufferUtil.bytes(v);
    }

    @Override
    public ByteBuffer valueOf(float v)
    {
        return ByteBufferUtil.bytes(v);
    }

    @Override
    public ByteBuffer valueOf(double v)
    {
        return ByteBufferUtil.bytes(v);
    }

    @Override
    public <V2> ByteBuffer convert(V2 src, ValueAccessor<V2> accessor)
    {
        return accessor.toBuffer(src);
    }

    @Override
    public ByteBuffer allocate(int size)
    {
        return ByteBuffer.allocate(size);
    }

    @Override
    public ObjectFactory<ByteBuffer> factory()
    {
        return ByteBufferObjectFactory.instance;
    }
}
