/*
 * 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.rows;

import java.nio.ByteBuffer;
import java.nio.ByteOrder;

import org.apache.cassandra.db.TypeSizes;
import org.apache.cassandra.db.marshal.AddressBasedNativeData;
import org.apache.cassandra.db.marshal.ByteArrayAccessor;
import org.apache.cassandra.db.marshal.NativeAccessor;
import org.apache.cassandra.db.marshal.NativeData;
import org.apache.cassandra.db.marshal.ValueAccessor;
import org.apache.cassandra.schema.ColumnMetadata;
import org.apache.cassandra.utils.ByteBufferUtil;
import org.apache.cassandra.utils.ObjectSizes;
import org.apache.cassandra.utils.concurrent.OpOrder;
import org.apache.cassandra.utils.memory.AddressBasedAllocator;
import org.apache.cassandra.utils.memory.MemoryUtil;
import org.apache.cassandra.utils.memory.NativeEndianMemoryUtil;

public class NativeCell extends AbstractCell<NativeData> implements NativeData
{
    private static final long EMPTY_SIZE = ObjectSizes.measure(new NativeCell());

    private static final long HAS_CELLPATH = 0;
    private static final long TIMESTAMP = 1;
    private static final long TTL = 9;
    private static final long DELETION = 13;
    private static final long LENGTH = 17;
    private static final long VALUE = 21;

    private final long peer;

    public static int estimateAllocationSize(Cell<?> cell)
    {
        long size = offHeapSizeWithoutPath(cell.valueSize());
        CellPath path = cell.path();
        if (path != null)
        {
            assert path.size() == 1 : String.format("Expected path size to be 1 but was not; %s", path);
            size += 4 + path.get(0).remaining();
        }

        if (size > Integer.MAX_VALUE)
            throw new IllegalStateException();
        return (int) size;
    }

    private NativeCell()
    {
        super(null);
        this.peer = 0;
    }

    public static NativeCell build(AddressBasedAllocator allocator,
                                   OpOrder.Group writeOp,
                                   Cell<?> cell)
    {
        if (cell.accessor() == ByteArrayAccessor.instance) // to avoid ByteBuffer allocation via cell.value()
        {
            byte[] value = cell.valueAsArray();
            return new NativeCell(allocator,
                                  writeOp,
                                  cell.column(),
                                  cell.timestamp(),
                                  cell.ttl(),
                                  cell.localDeletionTimeAsUnsignedInt(),
                                  value,
                                  value.length,
                                  cell.path());
        }
        else
        {
            ByteBuffer byteBuffer = cell.buffer();
            assert byteBuffer.order() == ByteOrder.BIG_ENDIAN;
            return new NativeCell(allocator,
                                  writeOp,
                                  cell.column(),
                                  cell.timestamp(),
                                  cell.ttl(),
                                  cell.localDeletionTimeAsUnsignedInt(),
                                  byteBuffer,
                                  byteBuffer.remaining(),
                                  cell.path());
        }
    }

    // Please keep both int/long overloaded ctros public. Otherwise silent casts will mess timestamps when one is not
    // available.
    public NativeCell(AddressBasedAllocator allocator,
                      OpOrder.Group writeOp,
                      ColumnMetadata column,
                      long timestamp,
                      int ttl,
                      long localDeletionTime,
                      ByteBuffer value,
                      CellPath path)
    {
        this(allocator, writeOp, column, timestamp, ttl, deletionTimeLongToUnsignedInteger(localDeletionTime), value, value.remaining(), path);
    }

    public NativeCell(AddressBasedAllocator allocator,
                      OpOrder.Group writeOp,
                      ColumnMetadata column,
                      long timestamp,
                      int ttl,
                      int localDeletionTimeUnsignedInteger,
                      Object value,
                      int valueLength,
                      CellPath path)
    {
        super(column);
        long size = offHeapSizeWithoutPath(valueLength);

        assert column.isComplex() == (path != null);
        if (path != null)
        {
            assert path.size() == 1 : String.format("Expected path size to be 1 but was not; %s", path);
            size += 4 + path.get(0).remaining();
        }

        if (size > Integer.MAX_VALUE)
            throw new IllegalStateException();

        // cellpath? : timestamp : ttl : localDeletionTime : length : <data> : [cell path length] : [<cell path data>]
        peer = allocator.allocate((int) size, writeOp);
        NativeEndianMemoryUtil.setByte(peer + HAS_CELLPATH, (byte)(path == null ? 0 : 1));
        NativeEndianMemoryUtil.setLong(peer + TIMESTAMP, timestamp);
        NativeEndianMemoryUtil.setInt(peer + TTL, ttl);
        NativeEndianMemoryUtil.setInt(peer + DELETION, localDeletionTimeUnsignedInteger);
        NativeEndianMemoryUtil.setInt(peer + LENGTH, valueLength);
        if (value instanceof byte[])
            MemoryUtil.setBytes(peer + VALUE, (byte[]) value, 0, valueLength);
        else if (value instanceof ByteBuffer)
            MemoryUtil.setBytes(peer + VALUE, (ByteBuffer) value);
        else
            throw new IllegalArgumentException();

        if (path != null)
        {
            ByteBuffer pathbuffer = path.get(0);
            assert pathbuffer.order() == ByteOrder.BIG_ENDIAN;

            long offset = peer + VALUE + valueLength;
            NativeEndianMemoryUtil.setInt(offset, pathbuffer.remaining());
            MemoryUtil.setBytes(offset + 4, pathbuffer);
        }
    }

    private static long offHeapSizeWithoutPath(int length)
    {
        return VALUE + length;
    }

    public long timestamp()
    {
        return NativeEndianMemoryUtil.getLong(peer + TIMESTAMP);
    }

    public int ttl()
    {
        return NativeEndianMemoryUtil.getInt(peer + TTL);
    }

    public NativeData value()
    {
        return this;
    }

    public ByteBuffer byteBufferValue()
    {
        int length = valueSize();
        return MemoryUtil.getByteBuffer(getAddress(), length, ByteOrder.BIG_ENDIAN);
    }

    public ValueAccessor<NativeData> accessor()
    {
        return NativeAccessor.instance;
    }

    public int valueSize()
    {
        return NativeEndianMemoryUtil.getInt(peer + LENGTH);
    }

    public int dataSize()
    {
        // NOTE: TypeSizes.sizeof(localDeletionTime()) - method calls like these are not eliminated by JIT in case of a megamorphic call
        return TypeSizes.LONG_SIZE   // timestamp()
               + TypeSizes.INT_SIZE  // ttl()
               + TypeSizes.LONG_SIZE // localDeletionTime()
               + valueSize()
               + (hasPath() ? pathDataSize() : 0);
    }

    public CellPath path()
    {
        if (!hasPath())
            return null;

        long offset = getAddress() + valueSize();
        int size = NativeEndianMemoryUtil.getInt(offset);
        return CellPath.create(MemoryUtil.getByteBuffer(offset + 4, size, ByteOrder.BIG_ENDIAN));
    }

    private int pathDataSize()
    {
        long offset = getAddress() + valueSize();
        return NativeEndianMemoryUtil.getInt(offset);
    }

    public Cell<?> withUpdatedValue(ByteBuffer newValue)
    {
        throw new UnsupportedOperationException();
    }

    @Override
    public Cell<?> withUpdatedTimestamp(long newTimestamp)
    {
        return new BufferCell(column, newTimestamp, ttl(), localDeletionTime(), byteBufferValue(), path());
    }

    public Cell<?> withUpdatedTimestampAndLocalDeletionTime(long newTimestamp, long newLocalDeletionTime)
    {
        return new BufferCell(column, newTimestamp, ttl(), newLocalDeletionTime, byteBufferValue(), path());
    }

    public Cell<?> withUpdatedColumn(ColumnMetadata column)
    {
        return new BufferCell(column, timestamp(), ttl(), localDeletionTimeAsUnsignedInt(), byteBufferValue(), path());
    }

    public Cell<?> withSkippedValue()
    {
        return new BufferCell(column, timestamp(), ttl(), localDeletionTimeAsUnsignedInt(), ByteBufferUtil.EMPTY_BYTE_BUFFER, path());
    }

    @Override
    public long unsharedHeapSize()
    {
        return EMPTY_SIZE;
    }

    @Override
    public long unsharedHeapSizeExcludingData()
    {
        return EMPTY_SIZE;
    }

    public long offHeapSize()
    {
        long size = offHeapSizeWithoutPath(NativeEndianMemoryUtil.getInt(peer + LENGTH));
        if (hasPath())
            size += 4 + NativeEndianMemoryUtil.getInt(peer + size);
        return size;
    }

    private boolean hasPath()
    {
        return NativeEndianMemoryUtil.getByte(peer + HAS_CELLPATH) != 0;
    }

    @Override
    protected int localDeletionTimeAsUnsignedInt()
    {
        return NativeEndianMemoryUtil.getInt(peer + DELETION);
    }


    @Override
    public int nativeDataSize()
    {
        return valueSize();
    }

    @Override
    public ByteBuffer asByteBuffer()
    {
        return byteBufferValue();
    }
    @Override
    public NativeData slice(int offset, int length)
    {
        return new AddressBasedNativeData(getAddress() + offset, length);
    }

    @Override
    public long getAddress()
    {
        return peer + VALUE;
    }
}
