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

import java.io.DataInput;
import java.io.IOException;
import java.io.InputStream;

import com.google.common.annotations.VisibleForTesting;

import org.apache.cassandra.db.TypeSizes;
import org.apache.cassandra.io.util.DataOutputPlus;
import org.apache.cassandra.io.util.Memory;
import org.apache.cassandra.io.util.MemoryOutputStream;
import org.apache.cassandra.utils.FBUtilities;
import org.apache.cassandra.utils.concurrent.Ref;

/**
 * Off-heap bitset,
 * file compatible with OpeBitSet
 */
public class OffHeapBitSet implements IBitSet
{
    private final Memory bytes;

    public OffHeapBitSet(long numBits)
    {
        /** returns the number of 64 bit words it would take to hold numBits */
        long wordCount = (((numBits - 1) >>> 6) + 1);
        if (wordCount > Integer.MAX_VALUE)
            throw new UnsupportedOperationException("Bloom filter size is > 16GB, reduce the bloom_filter_fp_chance");
        try
        {
            long byteCount = wordCount * 8L;
            bytes = Memory.allocate(byteCount);
        }
        catch (OutOfMemoryError e)
        {
            throw new RuntimeException("Out of native memory occured, You can avoid it by increasing the system ram space or by increasing bloom_filter_fp_chance.");
        }
        // flush/clear the existing memory.
        clear();
    }

    private OffHeapBitSet(Memory bytes)
    {
        this.bytes = bytes;
    }

    public long capacity()
    {
        return bytes.size() * 8;
    }

    @Override
    public long offHeapSize()
    {
        return bytes.size();
    }

    public void addTo(Ref.IdentityCollection identities)
    {
        identities.add(bytes);
    }

    public boolean get(long index)
    {
        long i = index >> 3;
        long bit = index & 0x7;
        int bitmask = 0x1 << bit;
        return (bytes.getByte(i) & bitmask) != 0;
    }

    public void set(long index)
    {
        long i = index >> 3;
        long bit = index & 0x7;
        int bitmask = 0x1 << bit;
        bytes.setByte(i, (byte) (bitmask | bytes.getByte(i)));
    }

    public void set(long offset, byte b)
    {
        bytes.setByte(offset, b);
    }

    public void clear(long index)
    {
        long i = index >> 3;
        long bit = index & 0x7;
        int bitmask = 0x1 << bit;
        int nativeByte = (bytes.getByte(i) & 0xFF);
        nativeByte &= ~bitmask;
        bytes.setByte(i, (byte) nativeByte);
    }

    public void clear()
    {
        bytes.setMemory(0, bytes.size(), (byte) 0);
    }

    public void serialize(DataOutputPlus out) throws IOException
    {
        out.writeInt((int) (bytes.size() / 8));
        out.write(bytes, 0, bytes.size());
    }

    @VisibleForTesting
    public void serializeOldBfFormat(DataOutputPlus out) throws IOException
    {
        out.writeInt((int) (bytes.size() / 8));
        for (long i = 0; i < bytes.size(); )
        {
            long value = ((bytes.getByte(i++) & 0xff) << 0)
                         + ((bytes.getByte(i++) & 0xff) << 8)
                         + ((bytes.getByte(i++) & 0xff) << 16)
                         + ((long) (bytes.getByte(i++) & 0xff) << 24)
                         + ((long) (bytes.getByte(i++) & 0xff) << 32)
                         + ((long) (bytes.getByte(i++) & 0xff) << 40)
                         + ((long) (bytes.getByte(i++) & 0xff) << 48)
                         + ((long) bytes.getByte(i++) << 56);
            out.writeLong(value);
        }
    }

    public long serializedSize()
    {
        return TypeSizes.sizeof((int) bytes.size()) + bytes.size();
    }

    public static <I extends InputStream & DataInput> OffHeapBitSet deserialize(I in, boolean oldBfFormat) throws IOException
    {
        long byteCount = in.readInt() * 8L;
        Memory memory = Memory.allocate(byteCount);
        if (oldBfFormat)
        {
            for (long i = 0; i < byteCount; )
            {
                long v = in.readLong();
                memory.setByte(i++, (byte) (v >>> 0));
                memory.setByte(i++, (byte) (v >>> 8));
                memory.setByte(i++, (byte) (v >>> 16));
                memory.setByte(i++, (byte) (v >>> 24));
                memory.setByte(i++, (byte) (v >>> 32));
                memory.setByte(i++, (byte) (v >>> 40));
                memory.setByte(i++, (byte) (v >>> 48));
                memory.setByte(i++, (byte) (v >>> 56));
            }
        }
        else
        {
            FBUtilities.copy(in, new MemoryOutputStream(memory), byteCount);
        }
        return new OffHeapBitSet(memory);
    }

    public void close()
    {
        bytes.free();
    }

    @Override
    public boolean equals(Object o)
    {
        if (this == o)
            return true;
        if (!(o instanceof OffHeapBitSet))
            return false;
        OffHeapBitSet b = (OffHeapBitSet) o;
        return bytes.equals(b.bytes);
    }

    @Override
    public int hashCode()
    {
        // Similar to open bitset.
        long h = 0;
        for (long i = bytes.size(); --i >= 0;)
        {
            h ^= bytes.getByte(i);
            h = (h << 1) | (h >>> 63); // rotate left
        }
        return (int) ((h >> 32) ^ h) + 0x98761234;
    }

    public String toString()
    {
        return "[OffHeapBitSet]";
    }
}
