// 
// Decompiled by Procyon v0.6.0
// 

package com.hypixel.hytale.storage;

import java.nio.charset.StandardCharsets;
import com.hypixel.hytale.codec.Codec;
import com.hypixel.hytale.unsafe.UnsafeUtil;
import java.nio.channels.FileLock;
import com.github.luben.zstd.Zstd;
import it.unimi.dsi.fastutil.ints.IntArrayList;
import it.unimi.dsi.fastutil.ints.IntList;
import java.util.logging.Level;
import java.util.Arrays;
import java.nio.file.StandardOpenOption;
import java.io.IOException;
import java.util.Set;
import java.nio.file.OpenOption;
import javax.annotation.Nullable;
import java.nio.MappedByteBuffer;
import java.nio.channels.FileChannel;
import javax.annotation.Nonnull;
import java.nio.file.Path;
import java.nio.ByteBuffer;
import java.nio.file.attribute.FileAttribute;
import com.hypixel.hytale.metrics.MetricsRegistry;
import java.util.concurrent.locks.StampedLock;
import com.hypixel.hytale.logger.HytaleLogger;
import java.io.Closeable;

@Deprecated
public class IndexedStorageFile_v0 implements Closeable
{
    private static final HytaleLogger LOGGER;
    public static final StampedLock[] EMPTY_STAMPED_LOCKS;
    public static final MetricsRegistry<IndexedStorageFile_v0> METRICS_REGISTRY;
    public static final String MAGIC_STRING = "HytaleIndexedStorage";
    public static final int VERSION = 0;
    public static final int DEFAULT_BLOB_COUNT = 1024;
    public static final int DEFAULT_SEGMENT_SIZE = 4096;
    public static final int DEFAULT_COMPRESSION_LEVEL = 3;
    public static final boolean DEFAULT_CONTIGUOUS_BLOBS = true;
    static final OffsetHelper HOH;
    public static final int MAGIC_LENGTH = 20;
    public static final int MAGIC_OFFSET;
    public static final int VERSION_OFFSET;
    public static final int BLOB_COUNT_OFFSET;
    public static final int SEGMENT_SIZE_OFFSET;
    public static final int HEADER_LENGTH;
    static final OffsetHelper SOH;
    public static final int NEXT_SEGMENT_OFFSET;
    public static final int SEGMENT_HEADER_LENGTH;
    static final OffsetHelper BOH;
    public static final int SRC_LENGTH_OFFSET;
    public static final int COMPRESSED_LENGTH_OFFSET;
    public static final int BLOB_HEADER_LENGTH;
    public static final int INDEX_SIZE = 4;
    public static final int UNASSIGNED_INDEX = 0;
    public static final int END_BLOB_INDEX = Integer.MIN_VALUE;
    public static final int FIRST_SEGMENT_INDEX = 1;
    public static final FileAttribute<?>[] NO_ATTRIBUTES;
    static final byte[] MAGIC_BYTES;
    private static final ByteBuffer MAGIC_BUFFER;
    private static final ThreadLocal<ByteBuffer> CACHED_TEMP_BUFFER;
    @Nonnull
    private final Path path;
    private final FileChannel fileChannel;
    private int compressionLevel;
    private boolean contiguousBlobs;
    private int blobCount;
    private int segmentSize;
    private StampedLock[] indexLocks;
    @Nullable
    private MappedByteBuffer mappedBlobIndexes;
    private final StampedLock segmentLocksLock;
    private StampedLock[] segmentLocks;
    private final StampedLock nextSegmentIndexesLock;
    @Nonnull
    private int[] nextSegmentIndexes;
    
    @Nonnull
    private static ByteBuffer getTempBuffer(final int length) {
        final ByteBuffer buffer = IndexedStorageFile_v0.CACHED_TEMP_BUFFER.get();
        buffer.position(0);
        buffer.limit(length);
        return buffer;
    }
    
    @Nonnull
    private static ByteBuffer allocateDirect(final int length) {
        return ByteBuffer.allocateDirect(length);
    }
    
    @Nonnull
    public static IndexedStorageFile_v0 open(@Nonnull final Path path, final OpenOption... options) throws IOException {
        return open(path, 1024, 4096, Set.of(options), IndexedStorageFile_v0.NO_ATTRIBUTES);
    }
    
    @Nonnull
    public static IndexedStorageFile_v0 open(@Nonnull final Path path, @Nonnull final Set<? extends OpenOption> options, final FileAttribute<?>... attrs) throws IOException {
        return open(path, 1024, 4096, options, attrs);
    }
    
    @Nonnull
    public static IndexedStorageFile_v0 open(@Nonnull final Path path, final int blobCount, final int segmentSize, final OpenOption... options) throws IOException {
        return open(path, blobCount, segmentSize, Set.of(options), IndexedStorageFile_v0.NO_ATTRIBUTES);
    }
    
    @Nonnull
    public static IndexedStorageFile_v0 open(@Nonnull final Path path, final int blobCount, final int segmentSize, @Nonnull final Set<? extends OpenOption> options, final FileAttribute<?>... attrs) throws IOException {
        final IndexedStorageFile_v0 storageFile = new IndexedStorageFile_v0(path, options, attrs);
        if (options.contains(StandardOpenOption.CREATE_NEW)) {
            storageFile.create(blobCount, segmentSize);
            return storageFile;
        }
        if (options.contains(StandardOpenOption.CREATE) && storageFile.fileChannel.size() == 0L) {
            storageFile.create(blobCount, segmentSize);
        }
        else {
            storageFile.open();
        }
        return storageFile;
    }
    
    private IndexedStorageFile_v0(@Nonnull final Path path, final Set<? extends OpenOption> options, final FileAttribute<?>[] attrs) throws IOException {
        this.compressionLevel = 3;
        this.contiguousBlobs = true;
        this.segmentLocksLock = new StampedLock();
        this.segmentLocks = IndexedStorageFile_v0.EMPTY_STAMPED_LOCKS;
        this.nextSegmentIndexesLock = new StampedLock();
        this.nextSegmentIndexes = new int[0];
        this.path = path;
        this.fileChannel = FileChannel.open(path, options, attrs);
    }
    
    IndexedStorageFile_v0(@Nonnull final Path path, final FileChannel fileChannel) throws IOException {
        this.compressionLevel = 3;
        this.contiguousBlobs = true;
        this.segmentLocksLock = new StampedLock();
        this.segmentLocks = IndexedStorageFile_v0.EMPTY_STAMPED_LOCKS;
        this.nextSegmentIndexesLock = new StampedLock();
        this.nextSegmentIndexes = new int[0];
        this.path = path;
        this.fileChannel = fileChannel;
    }
    
    @Nonnull
    public Path getPath() {
        return this.path;
    }
    
    public int getBlobCount() {
        return this.blobCount;
    }
    
    public int getSegmentSize() {
        return this.segmentSize;
    }
    
    public int getCompressionLevel() {
        return this.compressionLevel;
    }
    
    public void setCompressionLevel(final int compressionLevel) {
        this.compressionLevel = compressionLevel;
    }
    
    public boolean isContiguousBlobs() {
        return this.contiguousBlobs;
    }
    
    public void setContiguousBlobs(final boolean contiguousBlobs) {
        this.contiguousBlobs = contiguousBlobs;
    }
    
    @Nonnull
    protected IndexedStorageFile_v0 create(final int blobCount, final int segmentSize) throws IOException {
        if (blobCount <= 0) {
            throw new IllegalArgumentException("blobCount must be > 0");
        }
        if (segmentSize <= 0) {
            throw new IllegalArgumentException("segmentSize must be > 0");
        }
        this.blobCount = blobCount;
        this.segmentSize = segmentSize;
        if (this.fileChannel.size() != 0L) {
            throw new IOException("file channel is not empty");
        }
        this.writeHeader(blobCount, segmentSize);
        this.memoryMapBlobIndexes();
        return this;
    }
    
    protected void writeHeader(final int blobCount, final int segmentSize) throws IOException {
        final ByteBuffer header = getTempBuffer(IndexedStorageFile_v0.HEADER_LENGTH);
        header.put(IndexedStorageFile_v0.MAGIC_BYTES);
        header.putInt(IndexedStorageFile_v0.VERSION_OFFSET, 0);
        header.putInt(IndexedStorageFile_v0.BLOB_COUNT_OFFSET, blobCount);
        header.putInt(IndexedStorageFile_v0.SEGMENT_SIZE_OFFSET, segmentSize);
        header.position(0);
        if (this.fileChannel.write(header, 0L) != IndexedStorageFile_v0.HEADER_LENGTH) {
            throw new IllegalStateException();
        }
    }
    
    @Nonnull
    protected IndexedStorageFile_v0 open() throws IOException {
        if (this.fileChannel.size() == 0L) {
            throw new IOException("file channel is empty");
        }
        this.readHeader();
        this.memoryMapBlobIndexes();
        this.readNextIndexes();
        this.processTempIndexes();
        return this;
    }
    
    protected void readHeader() throws IOException {
        final ByteBuffer header = getTempBuffer(IndexedStorageFile_v0.HEADER_LENGTH);
        if (this.fileChannel.read(header, 0L) != IndexedStorageFile_v0.HEADER_LENGTH) {
            throw new IllegalStateException();
        }
        header.position(0);
        header.limit(20);
        if (!IndexedStorageFile_v0.MAGIC_BUFFER.equals(header)) {
            header.position(0);
            final byte[] dst = new byte[20];
            header.get(dst);
            throw new IOException("Invalid MAGIC! " + String.valueOf(header) + ", " + Arrays.toString(dst) + " expected " + Arrays.toString(IndexedStorageFile_v0.MAGIC_BYTES));
        }
        header.limit(IndexedStorageFile_v0.HEADER_LENGTH);
        final int version = header.getInt(IndexedStorageFile_v0.VERSION_OFFSET);
        if (version < 0 || version > 0) {
            throw new IOException("Invalid version! " + version);
        }
        this.blobCount = header.getInt(IndexedStorageFile_v0.BLOB_COUNT_OFFSET);
        this.segmentSize = header.getInt(IndexedStorageFile_v0.SEGMENT_SIZE_OFFSET);
    }
    
    protected void memoryMapBlobIndexes() throws IOException {
        this.indexLocks = new StampedLock[this.blobCount];
        for (int i = 0; i < this.blobCount; ++i) {
            this.indexLocks[i] = new StampedLock();
        }
        final int indexesSize = this.indexesLength() * 2;
        this.mappedBlobIndexes = this.fileChannel.map(FileChannel.MapMode.READ_WRITE, IndexedStorageFile_v0.HEADER_LENGTH, indexesSize);
    }
    
    protected void readNextIndexes() throws IOException {
        final ByteBuffer tempIndexBuffer = getTempBuffer(4);
        final long stamp = this.nextSegmentIndexesLock.writeLock();
        try {
            this.nextSegmentIndexes = new int[this.requiredSegments(this.fileChannel.size() - this.segmentsBase()) + 1];
            for (int segmentIndex = 1; segmentIndex < this.nextSegmentIndexes.length; ++segmentIndex) {
                tempIndexBuffer.position(0);
                if (this.fileChannel.read(tempIndexBuffer, this.segmentPosition(segmentIndex)) != 4) {
                    tempIndexBuffer.position(0);
                    tempIndexBuffer.putInt(0, 0);
                    this.fileChannel.write(tempIndexBuffer, this.segmentPosition(segmentIndex));
                    break;
                }
                this.nextSegmentIndexes[segmentIndex] = tempIndexBuffer.getInt(IndexedStorageFile_v0.NEXT_SEGMENT_OFFSET);
            }
        }
        finally {
            this.nextSegmentIndexesLock.unlockWrite(stamp);
        }
    }
    
    protected void processTempIndexes() throws IOException {
        int blobsCleared = 0;
        int segmentsCleared = 0;
        final ByteBuffer tempIndexBuffer = getTempBuffer(4);
        final int indexesLength = this.indexesLength();
        for (int blobIndex = 0; blobIndex < this.blobCount; ++blobIndex) {
            final int tempIndexPos = indexesLength + blobIndex * 4;
            final int firstSegmentIndex = this.mappedBlobIndexes.getInt(tempIndexPos);
            if (firstSegmentIndex != 0) {
                ++blobsCleared;
                segmentsCleared += this.clearSegments(firstSegmentIndex, tempIndexBuffer);
                this.mappedBlobIndexes.putInt(tempIndexPos, 0);
            }
        }
        if (blobsCleared != 0 || segmentsCleared != 0) {
            IndexedStorageFile_v0.LOGGER.at(Level.WARNING).log("Detected failed write for %s! Cleaned %s blobs with %s segments!", this.fileChannel, blobsCleared, segmentsCleared);
        }
    }
    
    protected int clearSegments(final int firstSegmentIndex, @Nonnull final ByteBuffer tempIndexBuffer) throws IOException {
        int[] segments = new int[8];
        int count = 0;
        int segmentIndex;
        for (int nextSegmentIndex = firstSegmentIndex; nextSegmentIndex != 0 && nextSegmentIndex != Integer.MIN_VALUE; nextSegmentIndex = segmentIndex) {
            if (count >= segments.length) {
                segments = Arrays.copyOf(segments, count * 2);
            }
            segments[count] = nextSegmentIndex;
            ++count;
            long indexesStamp = this.nextSegmentIndexesLock.tryOptimisticRead();
            segmentIndex = this.nextSegmentIndexes[nextSegmentIndex];
            if (!this.nextSegmentIndexesLock.validate(indexesStamp)) {
                indexesStamp = this.nextSegmentIndexesLock.readLock();
                try {
                    segmentIndex = this.nextSegmentIndexes[nextSegmentIndex];
                }
                finally {
                    this.nextSegmentIndexesLock.unlockRead(indexesStamp);
                }
            }
        }
        tempIndexBuffer.putInt(0, 0);
        for (int i = count - 1; i >= 0; --i) {
            tempIndexBuffer.position(0);
            final int segmentIndex2 = segments[i];
            final StampedLock segmentLock = this.getSegmentLock(segmentIndex2);
            final long segmentStamp = segmentLock.writeLock();
            try {
                if (this.fileChannel.write(tempIndexBuffer, this.segmentPosition(segmentIndex2)) != 4) {
                    throw new IllegalStateException();
                }
                final long indexesStamp2 = this.nextSegmentIndexesLock.writeLock();
                try {
                    this.nextSegmentIndexes[segmentIndex2] = 0;
                }
                finally {
                    this.nextSegmentIndexesLock.unlockWrite(indexesStamp2);
                }
            }
            finally {
                segmentLock.unlockWrite(segmentStamp);
            }
        }
        return count;
    }
    
    public long size() throws IOException {
        return this.fileChannel.size();
    }
    
    public int segmentSize() {
        long stamp = this.nextSegmentIndexesLock.tryOptimisticRead();
        final int value = this.nextSegmentIndexes.length;
        if (this.nextSegmentIndexesLock.validate(stamp)) {
            return value;
        }
        stamp = this.nextSegmentIndexesLock.readLock();
        try {
            return this.nextSegmentIndexes.length;
        }
        finally {
            this.nextSegmentIndexesLock.unlockRead(stamp);
        }
    }
    
    public int segmentCount() {
        long stamp = this.nextSegmentIndexesLock.tryOptimisticRead();
        int count = 0;
        int[] temp = this.nextSegmentIndexes;
        for (int i = 0; i < temp.length; ++i) {
            if (temp[i] != 0) {
                ++count;
            }
        }
        if (this.nextSegmentIndexesLock.validate(stamp)) {
            return count;
        }
        stamp = this.nextSegmentIndexesLock.readLock();
        try {
            count = 0;
            temp = this.nextSegmentIndexes;
            for (int i = 0; i < temp.length; ++i) {
                if (temp[i] != 0) {
                    ++count;
                }
            }
            return count;
        }
        finally {
            this.nextSegmentIndexesLock.unlockRead(stamp);
        }
    }
    
    @Nonnull
    public IntList keys() {
        final IntArrayList list = new IntArrayList(this.blobCount);
        for (int blobIndex = 0; blobIndex < this.blobCount; ++blobIndex) {
            final int indexPos = blobIndex * 4;
            final StampedLock lock = this.indexLocks[blobIndex];
            long stamp = lock.tryOptimisticRead();
            final int segmentIndex = this.mappedBlobIndexes.getInt(indexPos);
            if (lock.validate(stamp)) {
                if (segmentIndex != 0) {
                    list.add(blobIndex);
                }
            }
            else {
                stamp = lock.readLock();
                try {
                    if (this.mappedBlobIndexes.getInt(indexPos) != 0) {
                        list.add(blobIndex);
                    }
                }
                finally {
                    lock.unlockRead(stamp);
                }
            }
        }
        return list;
    }
    
    public int readBlobLength(final int blobIndex) throws IOException {
        if (blobIndex < 0 || blobIndex >= this.blobCount) {
            throw new IndexOutOfBoundsException("Index out of range: " + blobIndex + " blobCount: " + this.blobCount);
        }
        final int indexPos = blobIndex * 4;
        final long stamp = this.indexLocks[blobIndex].readLock();
        try {
            final int firstSegmentIndex = this.mappedBlobIndexes.getInt(indexPos);
            if (firstSegmentIndex == 0) {
                return 0;
            }
            final ByteBuffer blobHeaderBuffer = this.readBlobHeader(firstSegmentIndex);
            return blobHeaderBuffer.getInt(IndexedStorageFile_v0.SRC_LENGTH_OFFSET);
        }
        finally {
            this.indexLocks[blobIndex].unlockRead(stamp);
        }
    }
    
    public int readBlobCompressedLength(final int blobIndex) throws IOException {
        if (blobIndex < 0 || blobIndex >= this.blobCount) {
            throw new IndexOutOfBoundsException("Index out of range: " + blobIndex + " blobCount: " + this.blobCount);
        }
        final int indexPos = blobIndex * 4;
        final long stamp = this.indexLocks[blobIndex].readLock();
        try {
            final int firstSegmentIndex = this.mappedBlobIndexes.getInt(indexPos);
            if (firstSegmentIndex == 0) {
                return 0;
            }
            final ByteBuffer blobHeaderBuffer = this.readBlobHeader(firstSegmentIndex);
            return blobHeaderBuffer.getInt(IndexedStorageFile_v0.COMPRESSED_LENGTH_OFFSET);
        }
        finally {
            this.indexLocks[blobIndex].unlockRead(stamp);
        }
    }
    
    @Nullable
    public ByteBuffer readBlob(final int blobIndex) throws IOException {
        if (blobIndex < 0 || blobIndex >= this.blobCount) {
            throw new IndexOutOfBoundsException("Index out of range: " + blobIndex + " blobCount: " + this.blobCount);
        }
        final int indexPos = blobIndex * 4;
        final long stamp = this.indexLocks[blobIndex].readLock();
        int srcLength;
        ByteBuffer src;
        try {
            final int firstSegmentIndex = this.mappedBlobIndexes.getInt(indexPos);
            if (firstSegmentIndex == 0) {
                return null;
            }
            final ByteBuffer blobHeaderBuffer = this.readBlobHeader(firstSegmentIndex);
            srcLength = blobHeaderBuffer.getInt(IndexedStorageFile_v0.SRC_LENGTH_OFFSET);
            final int compressedLength = blobHeaderBuffer.getInt(IndexedStorageFile_v0.COMPRESSED_LENGTH_OFFSET);
            src = this.readSegments(firstSegmentIndex, compressedLength, blobHeaderBuffer);
        }
        finally {
            this.indexLocks[blobIndex].unlockRead(stamp);
        }
        src.position(0);
        return Zstd.decompress(src, srcLength);
    }
    
    public void readBlob(final int blobIndex, @Nonnull final ByteBuffer dest) throws IOException {
        if (blobIndex < 0 || blobIndex >= this.blobCount) {
            throw new IndexOutOfBoundsException("Index out of range: " + blobIndex + " blobCount: " + this.blobCount);
        }
        final int indexPos = blobIndex * 4;
        final long stamp = this.indexLocks[blobIndex].readLock();
        int srcLength;
        ByteBuffer src;
        try {
            final int firstSegmentIndex = this.mappedBlobIndexes.getInt(indexPos);
            if (firstSegmentIndex == 0) {
                return;
            }
            final ByteBuffer blobHeaderBuffer = this.readBlobHeader(firstSegmentIndex);
            srcLength = blobHeaderBuffer.getInt(IndexedStorageFile_v0.SRC_LENGTH_OFFSET);
            final int compressedLength = blobHeaderBuffer.getInt(IndexedStorageFile_v0.COMPRESSED_LENGTH_OFFSET);
            if (srcLength > dest.remaining()) {
                throw new IllegalArgumentException("dest buffer is not large enough! required dest.remaining() >= " + srcLength);
            }
            src = this.readSegments(firstSegmentIndex, compressedLength, blobHeaderBuffer);
        }
        finally {
            this.indexLocks[blobIndex].unlockRead(stamp);
        }
        src.position(0);
        if (dest.isDirect()) {
            Zstd.decompress(dest, src);
        }
        else {
            final ByteBuffer tempDest = allocateDirect(srcLength);
            Zstd.decompress(tempDest, src);
            tempDest.position(0);
            dest.put(tempDest);
        }
    }
    
    @Nonnull
    protected ByteBuffer readBlobHeader(final int firstSegmentIndex) throws IOException {
        if (firstSegmentIndex == 0) {
            throw new IllegalArgumentException("Invalid segment index!");
        }
        final ByteBuffer blobHeaderBuffer = getTempBuffer(IndexedStorageFile_v0.BLOB_HEADER_LENGTH);
        if (this.fileChannel.read(blobHeaderBuffer, this.blobHeaderPosition(firstSegmentIndex)) != IndexedStorageFile_v0.BLOB_HEADER_LENGTH) {
            throw new IllegalStateException();
        }
        return blobHeaderBuffer;
    }
    
    @Nonnull
    protected ByteBuffer readSegments(final int firstSegmentIndex, final int compressedLength, @Nonnull final ByteBuffer tempHeaderBuffer) throws IOException {
        tempHeaderBuffer.limit(IndexedStorageFile_v0.SEGMENT_HEADER_LENGTH);
        final ByteBuffer buffer = allocateDirect(compressedLength);
        int remainingBytes = compressedLength;
        int nextSegmentIndex = firstSegmentIndex;
        while (nextSegmentIndex != 0 && nextSegmentIndex != Integer.MIN_VALUE) {
            final long segmentPosition = this.segmentPosition(nextSegmentIndex);
            long indexesStamp = this.nextSegmentIndexesLock.tryOptimisticRead();
            int segmentIndex = this.nextSegmentIndexes[nextSegmentIndex];
            if (!this.nextSegmentIndexesLock.validate(indexesStamp)) {
                indexesStamp = this.nextSegmentIndexesLock.readLock();
                try {
                    segmentIndex = this.nextSegmentIndexes[nextSegmentIndex];
                }
                finally {
                    this.nextSegmentIndexesLock.unlockRead(indexesStamp);
                }
            }
            nextSegmentIndex = segmentIndex;
            int dataOffset = IndexedStorageFile_v0.SEGMENT_HEADER_LENGTH;
            if (buffer.position() == 0) {
                dataOffset += IndexedStorageFile_v0.BLOB_HEADER_LENGTH;
            }
            final int dataToRead = Math.min(this.segmentSize - dataOffset, remainingBytes);
            buffer.limit(buffer.position() + dataToRead);
            if (this.fileChannel.read(buffer, segmentPosition + dataOffset) != dataToRead) {
                throw new IllegalStateException();
            }
            remainingBytes -= dataToRead;
            if (remainingBytes == 0) {
                break;
            }
        }
        if (buffer.remaining() != 0 || nextSegmentIndex != Integer.MIN_VALUE) {
            throw new IOException("Failed to read segments: " + firstSegmentIndex + ", " + compressedLength + ", " + String.valueOf(buffer) + ", " + nextSegmentIndex);
        }
        return buffer;
    }
    
    public void writeBlob(final int blobIndex, @Nonnull final ByteBuffer src) throws IOException {
        if (blobIndex < 0 || blobIndex >= this.blobCount) {
            throw new IndexOutOfBoundsException("Index out of range: " + blobIndex + " blobCount: " + this.blobCount);
        }
        final int srcLength = src.remaining();
        final int maxCompressedLength = (int)Zstd.compressBound(srcLength);
        final ByteBuffer dest = allocateDirect(IndexedStorageFile_v0.BLOB_HEADER_LENGTH + maxCompressedLength);
        dest.putInt(IndexedStorageFile_v0.SRC_LENGTH_OFFSET, srcLength);
        dest.position(IndexedStorageFile_v0.BLOB_HEADER_LENGTH);
        int compressedLength;
        if (src.isDirect()) {
            compressedLength = Zstd.compress(dest, src, this.compressionLevel);
        }
        else {
            final ByteBuffer tempSrc = allocateDirect(srcLength);
            tempSrc.put(src);
            tempSrc.position(0);
            compressedLength = Zstd.compress(dest, tempSrc, this.compressionLevel);
        }
        dest.putInt(IndexedStorageFile_v0.COMPRESSED_LENGTH_OFFSET, compressedLength);
        dest.limit(dest.position());
        dest.position(0);
        final int indexPos = blobIndex * 4;
        final int tempIndexPos = this.indexesLength() + indexPos;
        final long stamp = this.indexLocks[blobIndex].writeLock();
        try {
            final int firstSegmentIndex = this.writeSegments(blobIndex, dest);
            final int oldFirstSegmentIndex = this.mappedBlobIndexes.getInt(indexPos);
            this.mappedBlobIndexes.putInt(indexPos, firstSegmentIndex);
            this.mappedBlobIndexes.putInt(tempIndexPos, oldFirstSegmentIndex);
            if (oldFirstSegmentIndex != 0) {
                final ByteBuffer tempIndexBuffer = getTempBuffer(4);
                this.clearSegments(oldFirstSegmentIndex, tempIndexBuffer);
                this.mappedBlobIndexes.putInt(tempIndexPos, 0);
            }
        }
        finally {
            this.indexLocks[blobIndex].unlockWrite(stamp);
        }
    }
    
    public void removeBlob(final int blobIndex) throws IOException {
        if (blobIndex < 0 || blobIndex >= this.blobCount) {
            throw new IndexOutOfBoundsException("Index out of range: " + blobIndex + " blobCount: " + this.blobCount);
        }
        final int indexPos = blobIndex * 4;
        final int tempIndexPos = this.indexesLength() + indexPos;
        final long stamp = this.indexLocks[blobIndex].writeLock();
        try {
            final int oldFirstSegmentIndex = this.mappedBlobIndexes.getInt(indexPos);
            if (oldFirstSegmentIndex != 0) {
                this.mappedBlobIndexes.putInt(indexPos, 0);
                this.mappedBlobIndexes.putInt(tempIndexPos, oldFirstSegmentIndex);
                final ByteBuffer tempIndexBuffer = getTempBuffer(4);
                this.clearSegments(oldFirstSegmentIndex, tempIndexBuffer);
                this.mappedBlobIndexes.putInt(tempIndexPos, 0);
            }
        }
        finally {
            this.indexLocks[blobIndex].unlockWrite(stamp);
        }
    }
    
    protected int writeSegments(final int blobIndex, @Nonnull final ByteBuffer data) throws IOException {
        int dataRemaining = data.remaining();
        final int indexPos = blobIndex * 4;
        final int tempIndexPos = this.indexesLength() + indexPos;
        final ByteBuffer tempIndexBuffer = getTempBuffer(4);
        if (this.contiguousBlobs) {
            final int segmentsCount = this.requiredSegments(dataRemaining);
            final SegmentRangeLock segmentLock = this.findFreeSegment(segmentsCount);
            try {
                final int firstSegmentIndex = segmentLock.segmentIndex;
                this.mappedBlobIndexes.putInt(tempIndexPos, firstSegmentIndex);
                final int endSegmentIndex = firstSegmentIndex + segmentsCount;
                final long indexesResizeStamp = this.nextSegmentIndexesLock.writeLock();
                try {
                    if (endSegmentIndex >= this.nextSegmentIndexes.length) {
                        this.nextSegmentIndexes = Arrays.copyOf(this.nextSegmentIndexes, endSegmentIndex);
                    }
                }
                finally {
                    this.nextSegmentIndexesLock.unlockWrite(indexesResizeStamp);
                }
                for (int segmentIndex = firstSegmentIndex; segmentIndex < endSegmentIndex; ++segmentIndex) {
                    final long segmentPosition = this.segmentPosition(segmentIndex);
                    final int next = segmentIndex + 1;
                    final int nextSegmentIndex = (next < endSegmentIndex) ? next : Integer.MIN_VALUE;
                    tempIndexBuffer.position(0);
                    tempIndexBuffer.putInt(0, nextSegmentIndex);
                    if (this.fileChannel.write(tempIndexBuffer, segmentPosition) != IndexedStorageFile_v0.SEGMENT_HEADER_LENGTH) {
                        throw new IllegalStateException();
                    }
                    final long indexesStamp = this.nextSegmentIndexesLock.writeLock();
                    try {
                        this.nextSegmentIndexes[segmentIndex] = nextSegmentIndex;
                    }
                    finally {
                        this.nextSegmentIndexesLock.unlockWrite(indexesStamp);
                    }
                    final int dataToWrite = Math.min(this.segmentSize - IndexedStorageFile_v0.SEGMENT_HEADER_LENGTH, dataRemaining);
                    data.limit(data.position() + dataToWrite);
                    if (this.fileChannel.write(data, segmentPosition + IndexedStorageFile_v0.SEGMENT_HEADER_LENGTH) != dataToWrite) {
                        throw new IllegalStateException();
                    }
                    dataRemaining -= dataToWrite;
                }
                return firstSegmentIndex;
            }
            finally {
                segmentLock.unlockWrite();
            }
        }
        throw new UnsupportedOperationException("Not implemented!");
    }
    
    @Nonnull
    private SegmentRangeLock findFreeSegment(final int count) {
        int start = 0;
        int index = 1;
        int found = 0;
        SegmentRangeLock segmentRangeLock = null;
    Label_0007:
        while (true) {
            if (found < count) {
                final StampedLock segmentLock = this.getSegmentLock(index);
                final long stamp = segmentLock.tryReadLock();
                if (stamp == 0L) {
                    start = 0;
                    found = 0;
                    ++index;
                }
                else {
                    int next;
                    try {
                        next = this.getNextIndex(index);
                    }
                    finally {
                        segmentLock.unlockRead(stamp);
                    }
                    if (next == 0) {
                        if (start == 0) {
                            start = index;
                        }
                        ++found;
                    }
                    else {
                        start = 0;
                        found = 0;
                    }
                    ++index;
                }
            }
            else {
                segmentRangeLock = this.tryWriteLockSegmentRange(start, count);
                if (segmentRangeLock != null) {
                    for (int i = count - 1; i >= 0; --i) {
                        final int segmentIndex = start + i;
                        final int next2 = this.getNextIndex(segmentIndex);
                        if (next2 != 0) {
                            segmentRangeLock.unlockWrite();
                            index = segmentIndex + 1;
                            start = 0;
                            found = 0;
                            continue Label_0007;
                        }
                    }
                    break;
                }
                ++start;
                --found;
            }
        }
        return segmentRangeLock;
    }
    
    protected int getNextIndex(final int segmentIndex) {
        long indexesStamp = this.nextSegmentIndexesLock.tryOptimisticRead();
        final int nextSegmentIndex = (segmentIndex < this.nextSegmentIndexes.length) ? this.nextSegmentIndexes[segmentIndex] : 0;
        if (this.nextSegmentIndexesLock.validate(indexesStamp)) {
            return nextSegmentIndex;
        }
        indexesStamp = this.nextSegmentIndexesLock.readLock();
        try {
            return (segmentIndex < this.nextSegmentIndexes.length) ? this.nextSegmentIndexes[segmentIndex] : 0;
        }
        finally {
            this.nextSegmentIndexesLock.unlockRead(indexesStamp);
        }
    }
    
    protected StampedLock getSegmentLock(final int segmentIndex) {
        if (segmentIndex < this.segmentLocks.length) {
            return this.segmentLocks[segmentIndex];
        }
        final long stamp = this.segmentLocksLock.writeLock();
        try {
            if (segmentIndex < this.segmentLocks.length) {
                return this.segmentLocks[segmentIndex];
            }
            final int newLength = segmentIndex + 1;
            final StampedLock[] newArray = Arrays.copyOf(this.segmentLocks, newLength);
            for (int i = this.segmentLocks.length; i < newLength; ++i) {
                newArray[i] = new StampedLock();
            }
            this.segmentLocks = newArray;
            return this.segmentLocks[segmentIndex];
        }
        finally {
            this.segmentLocksLock.unlockWrite(stamp);
        }
    }
    
    @Nullable
    protected SegmentRangeLock tryWriteLockSegmentRange(final int start, final int count) {
        final long[] stamps = new long[count];
        for (int i = 0; i < count; ++i) {
            final StampedLock segmentLock = this.getSegmentLock(start + i);
            if (segmentLock.isWriteLocked()) {
                for (int i2 = 0; i2 < i; ++i2) {
                    this.getSegmentLock(start + i2).unlockWrite(stamps[i2]);
                }
                return null;
            }
            stamps[i] = segmentLock.writeLock();
        }
        return new SegmentRangeLock(start, count, stamps);
    }
    
    protected int indexesLength() {
        return this.blobCount * 4;
    }
    
    protected long segmentsBase() {
        return IndexedStorageFile_v0.HEADER_LENGTH + this.indexesLength() * 2L;
    }
    
    protected long segmentOffset(final int segmentIndex) {
        if (segmentIndex == 0) {
            throw new IllegalArgumentException("Invalid segment index!");
        }
        return (segmentIndex - 1) * (long)this.segmentSize;
    }
    
    protected long segmentPosition(final int segmentIndex) {
        return this.segmentOffset(segmentIndex) + this.segmentsBase();
    }
    
    protected int positionToSegment(final long position) {
        final long segmentOffset = position - this.segmentsBase();
        if (segmentOffset < 0L) {
            throw new IllegalArgumentException("position is before the segments start");
        }
        return (int)(segmentOffset / this.segmentSize) + 1;
    }
    
    protected long blobHeaderPosition(final int segmentIndex) {
        return this.segmentPosition(segmentIndex) + IndexedStorageFile_v0.SEGMENT_HEADER_LENGTH;
    }
    
    protected int requiredSegments(final long dataLength) {
        final int size = this.segmentSize - IndexedStorageFile_v0.SEGMENT_HEADER_LENGTH;
        return (int)((dataLength + size - 1L) / size);
    }
    
    public FileLock lock() throws IOException {
        return this.fileChannel.lock();
    }
    
    public void force(final boolean metaData) throws IOException {
        this.mappedBlobIndexes.force();
        this.fileChannel.force(metaData);
    }
    
    @Override
    public void close() throws IOException {
        this.fileChannel.close();
        if (UnsafeUtil.UNSAFE != null) {
            UnsafeUtil.UNSAFE.invokeCleaner(this.mappedBlobIndexes);
        }
        this.mappedBlobIndexes = null;
    }
    
    @Nonnull
    @Override
    public String toString() {
        return "IndexedStorageFile{fileChannel=" + String.valueOf(this.fileChannel) + ", compressionLevel=" + this.compressionLevel + ", contiguousBlobs=" + this.contiguousBlobs + ", blobCount=" + this.blobCount + ", segmentSize=" + this.segmentSize + ", mappedBlobIndexes=" + String.valueOf(this.mappedBlobIndexes) + ", nextSegmentIndexes=" + Arrays.toString(this.nextSegmentIndexes);
    }
    
    static {
        LOGGER = HytaleLogger.forEnclosingClass();
        EMPTY_STAMPED_LOCKS = new StampedLock[0];
        METRICS_REGISTRY = new MetricsRegistry<IndexedStorageFile_v0>().register("Size", file -> {
            try {
                return Long.valueOf(file.size());
            }
            catch (final IOException e) {
                return Long.valueOf(-1L);
            }
        }, (Codec<Long>)Codec.LONG).register("CompressionLevel", file -> file.getCompressionLevel(), (Codec<Integer>)Codec.INTEGER).register("ContiguousBlobs", file -> file.isContiguousBlobs(), (Codec<Boolean>)Codec.BOOLEAN).register("BlobCount", file -> file.getBlobCount(), (Codec<Integer>)Codec.INTEGER).register("UsedBlobCount", file -> file.keys().size(), (Codec<Integer>)Codec.INTEGER).register("SegmentSize", file -> file.segmentSize(), (Codec<Integer>)Codec.INTEGER).register("SegmentCount", file -> file.segmentCount(), (Codec<Integer>)Codec.INTEGER);
        HOH = new OffsetHelper();
        MAGIC_OFFSET = IndexedStorageFile_v0.HOH.next(20);
        VERSION_OFFSET = IndexedStorageFile_v0.HOH.next(4);
        BLOB_COUNT_OFFSET = IndexedStorageFile_v0.HOH.next(4);
        SEGMENT_SIZE_OFFSET = IndexedStorageFile_v0.HOH.next(4);
        HEADER_LENGTH = IndexedStorageFile_v0.HOH.length();
        SOH = new OffsetHelper();
        NEXT_SEGMENT_OFFSET = IndexedStorageFile_v0.SOH.next(4);
        SEGMENT_HEADER_LENGTH = IndexedStorageFile_v0.SOH.length();
        BOH = new OffsetHelper();
        SRC_LENGTH_OFFSET = IndexedStorageFile_v0.BOH.next(4);
        COMPRESSED_LENGTH_OFFSET = IndexedStorageFile_v0.BOH.next(4);
        BLOB_HEADER_LENGTH = IndexedStorageFile_v0.BOH.length();
        NO_ATTRIBUTES = new FileAttribute[0];
        MAGIC_BYTES = "HytaleIndexedStorage".getBytes(StandardCharsets.UTF_8);
        (MAGIC_BUFFER = ByteBuffer.wrap(IndexedStorageFile_v0.MAGIC_BYTES)).position(0);
        CACHED_TEMP_BUFFER = ThreadLocal.withInitial(() -> ByteBuffer.allocateDirect(IndexedStorageFile_v0.HEADER_LENGTH));
    }
    
    static class OffsetHelper
    {
        private int index;
        
        public int next(final int len) {
            final int cur = this.index;
            this.index += len;
            return cur;
        }
        
        public int length() {
            return this.index;
        }
    }
    
    protected class SegmentRangeLock
    {
        private final int segmentIndex;
        private final int count;
        private final long[] stamps;
        
        public SegmentRangeLock(final int segmentIndex, final int count, final long[] stamps) {
            this.segmentIndex = segmentIndex;
            this.count = count;
            this.stamps = stamps;
        }
        
        protected void unlockRead() {
            for (int i = 0; i < this.count; ++i) {
                IndexedStorageFile_v0.this.getSegmentLock(this.segmentIndex + i).unlockRead(this.stamps[i]);
                this.stamps[i] = 0L;
            }
        }
        
        protected void unlockWrite() {
            for (int i = 0; i < this.count; ++i) {
                IndexedStorageFile_v0.this.getSegmentLock(this.segmentIndex + i).unlockWrite(this.stamps[i]);
                this.stamps[i] = 0L;
            }
        }
    }
}
