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

package com.hypixel.hytale.storage;

import java.nio.charset.StandardCharsets;
import com.hypixel.hytale.codec.Codec;
import java.nio.channels.FileLock;
import com.hypixel.hytale.unsafe.UnsafeUtil;
import javax.annotation.Nullable;
import com.github.luben.zstd.Zstd;
import it.unimi.dsi.fastutil.ints.IntArrayList;
import it.unimi.dsi.fastutil.ints.IntList;
import java.util.Arrays;
import java.util.Collection;
import java.util.HashSet;
import java.nio.file.Files;
import java.nio.file.StandardCopyOption;
import java.nio.file.CopyOption;
import java.nio.file.StandardOpenOption;
import java.io.IOException;
import java.util.Set;
import java.nio.file.OpenOption;
import java.util.BitSet;
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 java.io.Closeable;

public class IndexedStorageFile implements Closeable
{
    public static final StampedLock[] EMPTY_STAMPED_LOCKS;
    public static final MetricsRegistry<IndexedStorageFile> METRICS_REGISTRY;
    public static final String MAGIC_STRING = "HytaleIndexedStorage";
    public static final int VERSION = 1;
    public static final int DEFAULT_BLOB_COUNT = 1024;
    public static final int DEFAULT_SEGMENT_SIZE = 4096;
    public static final int DEFAULT_COMPRESSION_LEVEL = 3;
    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 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 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 boolean flushOnWrite;
    private int compressionLevel;
    private int version;
    private int blobCount;
    private int segmentSize;
    private StampedLock[] indexLocks;
    private MappedByteBuffer mappedBlobIndexes;
    private final StampedLock segmentLocksLock;
    private StampedLock[] segmentLocks;
    private final StampedLock usedSegmentsLock;
    private final BitSet usedSegments;
    
    @Nonnull
    private static ByteBuffer getTempBuffer(final int length) {
        final ByteBuffer buffer = IndexedStorageFile.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 open(@Nonnull final Path path, final OpenOption... options) throws IOException {
        return open(path, 1024, 4096, Set.of(options), IndexedStorageFile.NO_ATTRIBUTES);
    }
    
    @Nonnull
    public static IndexedStorageFile 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 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.NO_ATTRIBUTES);
    }
    
    @Nonnull
    public static IndexedStorageFile open(@Nonnull final Path path, final int blobCount, final int segmentSize, @Nonnull final Set<? extends OpenOption> options, final FileAttribute<?>... attrs) throws IOException {
        IndexedStorageFile storageFile = new IndexedStorageFile(path, FileChannel.open(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 {
            if (storageFile.fileChannel.size() == 0L) {
                throw new IOException("file channel is empty");
            }
            storageFile.readHeader();
            storageFile.memoryMapBlobIndexes();
            if (storageFile.version == 0) {
                storageFile = migrateV0(path, blobCount, segmentSize, options, attrs, storageFile);
            }
            else {
                storageFile.readUsedSegments();
            }
        }
        return storageFile;
    }
    
    private static IndexedStorageFile migrateV0(final Path path, final int blobCount, final int segmentSize, final Set<? extends OpenOption> options, final FileAttribute<?>[] attrs, IndexedStorageFile storageFile) throws IOException {
        storageFile.close();
        final Path tempFile = path.resolveSibling(path.getFileName().toString() + ".old");
        final Path tempPath = Files.move(path, tempFile, StandardCopyOption.REPLACE_EXISTING);
        final HashSet<OpenOption> newOptions = new HashSet<OpenOption>(options);
        newOptions.add(StandardOpenOption.CREATE);
        storageFile = new IndexedStorageFile(path, FileChannel.open(path, newOptions, attrs));
        storageFile.create(blobCount, segmentSize);
        try (final IndexedStorageFile_v0 oldStorageFile = new IndexedStorageFile_v0(tempPath, FileChannel.open(tempPath, options, attrs))) {
            oldStorageFile.open();
            for (int blobIndex = 0; blobIndex < blobCount; ++blobIndex) {
                final ByteBuffer blob = oldStorageFile.readBlob(blobIndex);
                if (blob != null) {
                    storageFile.writeBlob(blobIndex, blob);
                }
            }
        }
        finally {
            Files.delete(tempFile);
        }
        return storageFile;
    }
    
    private IndexedStorageFile(@Nonnull final Path path, @Nonnull final FileChannel fileChannel) {
        this.flushOnWrite = false;
        this.compressionLevel = 3;
        this.segmentLocksLock = new StampedLock();
        this.segmentLocks = IndexedStorageFile.EMPTY_STAMPED_LOCKS;
        this.usedSegmentsLock = new StampedLock();
        this.usedSegments = new BitSet();
        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 setFlushOnWrite(final boolean flushOnWrite) {
        this.flushOnWrite = flushOnWrite;
    }
    
    public void setCompressionLevel(final int compressionLevel) {
        this.compressionLevel = compressionLevel;
    }
    
    @Nonnull
    protected IndexedStorageFile 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.HEADER_LENGTH);
        header.put(IndexedStorageFile.MAGIC_BYTES);
        header.putInt(IndexedStorageFile.VERSION_OFFSET, 1);
        header.putInt(IndexedStorageFile.BLOB_COUNT_OFFSET, blobCount);
        header.putInt(IndexedStorageFile.SEGMENT_SIZE_OFFSET, segmentSize);
        header.position(0);
        if (this.fileChannel.write(header, 0L) != IndexedStorageFile.HEADER_LENGTH) {
            throw new IllegalStateException();
        }
    }
    
    protected void readHeader() throws IOException {
        final ByteBuffer header = getTempBuffer(IndexedStorageFile.HEADER_LENGTH);
        if (this.fileChannel.read(header, 0L) != IndexedStorageFile.HEADER_LENGTH) {
            throw new IllegalStateException();
        }
        header.position(0);
        header.limit(20);
        if (!IndexedStorageFile.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.MAGIC_BYTES));
        }
        header.limit(IndexedStorageFile.HEADER_LENGTH);
        this.version = header.getInt(IndexedStorageFile.VERSION_OFFSET);
        if (this.version < 0 || this.version > 1) {
            throw new IOException("Invalid version! " + this.version);
        }
        this.blobCount = header.getInt(IndexedStorageFile.BLOB_COUNT_OFFSET);
        this.segmentSize = header.getInt(IndexedStorageFile.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();
        }
        this.mappedBlobIndexes = this.fileChannel.map(FileChannel.MapMode.READ_WRITE, IndexedStorageFile.HEADER_LENGTH, this.blobCount * 4L);
    }
    
    protected void readUsedSegments() throws IOException {
        final long stamp = this.usedSegmentsLock.writeLock();
        try {
            for (int blobIndex = 0; blobIndex < this.blobCount; ++blobIndex) {
                final int indexPos = blobIndex * 4;
                final long segmentStamp = this.indexLocks[blobIndex].readLock();
                int firstSegmentIndex;
                int compressedLength;
                try {
                    firstSegmentIndex = this.mappedBlobIndexes.getInt(indexPos);
                    if (firstSegmentIndex == 0) {
                        compressedLength = 0;
                    }
                    else {
                        final ByteBuffer blobHeaderBuffer = this.readBlobHeader(firstSegmentIndex);
                        compressedLength = blobHeaderBuffer.getInt(IndexedStorageFile.COMPRESSED_LENGTH_OFFSET);
                    }
                }
                finally {
                    this.indexLocks[blobIndex].unlockRead(segmentStamp);
                }
                if (compressedLength > 0) {
                    final int segmentsCount = this.requiredSegments(IndexedStorageFile.BLOB_HEADER_LENGTH + compressedLength);
                    this.usedSegments.set(firstSegmentIndex, firstSegmentIndex + segmentsCount);
                }
            }
        }
        finally {
            this.usedSegmentsLock.unlockWrite(stamp);
        }
    }
    
    public long size() throws IOException {
        return this.fileChannel.size();
    }
    
    public int segmentSize() {
        try {
            return this.requiredSegments(this.fileChannel.size() - this.segmentsBase()) + 1;
        }
        catch (final IOException e) {
            return -1;
        }
    }
    
    public int segmentCount() {
        long stamp = this.usedSegmentsLock.tryOptimisticRead();
        final int count = this.usedSegments.cardinality();
        if (this.usedSegmentsLock.validate(stamp)) {
            return count;
        }
        stamp = this.usedSegmentsLock.readLock();
        try {
            return this.usedSegments.cardinality();
        }
        finally {
            this.usedSegmentsLock.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.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.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.SRC_LENGTH_OFFSET);
            final int compressedLength = blobHeaderBuffer.getInt(IndexedStorageFile.COMPRESSED_LENGTH_OFFSET);
            src = this.readSegments(firstSegmentIndex, compressedLength);
        }
        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.SRC_LENGTH_OFFSET);
            final int compressedLength = blobHeaderBuffer.getInt(IndexedStorageFile.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);
        }
        finally {
            this.indexLocks[blobIndex].unlockRead(stamp);
        }
        src.position(0);
        if (dest.isDirect()) {
            Zstd.decompress(dest, src);
        }
        else {
            final ByteBuffer tempDest = allocateDirect(srcLength);
            try {
                Zstd.decompress(tempDest, src);
                tempDest.position(0);
                dest.put(tempDest);
            }
            finally {
                if (UnsafeUtil.UNSAFE != null) {
                    UnsafeUtil.UNSAFE.invokeCleaner(tempDest);
                }
            }
        }
    }
    
    @Nonnull
    protected ByteBuffer readBlobHeader(final int firstSegmentIndex) throws IOException {
        if (firstSegmentIndex == 0) {
            throw new IllegalArgumentException("Invalid segment index!");
        }
        final ByteBuffer blobHeaderBuffer = getTempBuffer(IndexedStorageFile.BLOB_HEADER_LENGTH);
        if (this.fileChannel.read(blobHeaderBuffer, this.segmentPosition(firstSegmentIndex)) != IndexedStorageFile.BLOB_HEADER_LENGTH) {
            throw new IllegalStateException();
        }
        return blobHeaderBuffer;
    }
    
    @Nonnull
    protected ByteBuffer readSegments(final int firstSegmentIndex, final int compressedLength) throws IOException {
        final ByteBuffer buffer = allocateDirect(compressedLength);
        final long segmentPosition = this.segmentPosition(firstSegmentIndex);
        if (this.fileChannel.read(buffer, segmentPosition + IndexedStorageFile.BLOB_HEADER_LENGTH) != compressedLength) {
            throw new IllegalStateException();
        }
        if (buffer.remaining() != 0) {
            throw new IOException("Failed to read segments: " + firstSegmentIndex + ", " + compressedLength + ", " + String.valueOf(buffer));
        }
        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.BLOB_HEADER_LENGTH + maxCompressedLength);
        dest.putInt(IndexedStorageFile.SRC_LENGTH_OFFSET, srcLength);
        dest.position(IndexedStorageFile.BLOB_HEADER_LENGTH);
        int compressedLength;
        if (src.isDirect()) {
            compressedLength = Zstd.compress(dest, src, this.compressionLevel);
        }
        else {
            final ByteBuffer tempSrc = allocateDirect(srcLength);
            try {
                tempSrc.put(src);
                tempSrc.position(0);
                compressedLength = Zstd.compress(dest, tempSrc, this.compressionLevel);
            }
            finally {
                if (UnsafeUtil.UNSAFE != null) {
                    UnsafeUtil.UNSAFE.invokeCleaner(tempSrc);
                }
            }
        }
        dest.putInt(IndexedStorageFile.COMPRESSED_LENGTH_OFFSET, compressedLength);
        dest.limit(dest.position());
        dest.position(0);
        final int indexPos = blobIndex * 4;
        final long stamp = this.indexLocks[blobIndex].writeLock();
        try {
            int oldSegmentLength = 0;
            final int oldFirstSegmentIndex = this.mappedBlobIndexes.getInt(indexPos);
            if (oldFirstSegmentIndex != 0) {
                final ByteBuffer blobHeaderBuffer = this.readBlobHeader(oldFirstSegmentIndex);
                final int oldCompressedLength = blobHeaderBuffer.getInt(IndexedStorageFile.COMPRESSED_LENGTH_OFFSET);
                oldSegmentLength = this.requiredSegments(IndexedStorageFile.BLOB_HEADER_LENGTH + oldCompressedLength);
            }
            final int firstSegmentIndex = this.writeSegments(dest);
            if (this.flushOnWrite) {
                this.fileChannel.force(false);
            }
            this.mappedBlobIndexes.putInt(indexPos, firstSegmentIndex);
            if (this.flushOnWrite) {
                this.mappedBlobIndexes.force(indexPos, 4);
            }
            if (oldSegmentLength > 0) {
                final long usedSegmentsStamp = this.usedSegmentsLock.writeLock();
                try {
                    this.usedSegments.clear(oldFirstSegmentIndex, oldFirstSegmentIndex + oldSegmentLength);
                }
                finally {
                    this.usedSegmentsLock.unlockWrite(usedSegmentsStamp);
                }
            }
        }
        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 long stamp = this.indexLocks[blobIndex].writeLock();
        try {
            final int oldFirstSegmentIndex = this.mappedBlobIndexes.getInt(indexPos);
            if (oldFirstSegmentIndex != 0) {
                final ByteBuffer blobHeaderBuffer = this.readBlobHeader(oldFirstSegmentIndex);
                final int oldCompressedLength = blobHeaderBuffer.getInt(IndexedStorageFile.COMPRESSED_LENGTH_OFFSET);
                final int oldSegmentLength = this.requiredSegments(IndexedStorageFile.BLOB_HEADER_LENGTH + oldCompressedLength);
                this.mappedBlobIndexes.putInt(indexPos, 0);
                if (this.flushOnWrite) {
                    this.mappedBlobIndexes.force(indexPos, 4);
                }
                final long usedSegmentsStamp = this.usedSegmentsLock.writeLock();
                try {
                    this.usedSegments.clear(oldFirstSegmentIndex, oldFirstSegmentIndex + oldSegmentLength);
                }
                finally {
                    this.usedSegmentsLock.unlockWrite(usedSegmentsStamp);
                }
            }
        }
        finally {
            this.indexLocks[blobIndex].unlockWrite(stamp);
        }
    }
    
    protected int writeSegments(@Nonnull final ByteBuffer data) throws IOException {
        final int dataRemaining = data.remaining();
        final int segmentsCount = this.requiredSegments(dataRemaining);
        final SegmentRangeWriteLock segmentLock = this.findFreeSegment(segmentsCount);
        try {
            final int firstSegmentIndex = segmentLock.segmentIndex;
            if (this.fileChannel.write(data, this.segmentPosition(firstSegmentIndex)) != dataRemaining) {
                throw new IllegalStateException();
            }
            final long stamp = this.usedSegmentsLock.writeLock();
            try {
                this.usedSegments.set(firstSegmentIndex, firstSegmentIndex + segmentsCount);
            }
            finally {
                this.usedSegmentsLock.unlockWrite(stamp);
            }
            return firstSegmentIndex;
        }
        finally {
            segmentLock.unlock();
        }
    }
    
    @Nonnull
    private SegmentRangeWriteLock findFreeSegment(final int count) {
        final long[] stamps = new long[count];
        int index = 1;
    Label_0006:
        while (true) {
            final long indexesStamp = this.usedSegmentsLock.readLock();
            try {
                int start = 0;
                int nextUsedIndex;
                for (int found = 0; found < count; found = nextUsedIndex - start, index = nextUsedIndex + 1) {
                    nextUsedIndex = this.usedSegments.nextSetBit(index);
                    if (nextUsedIndex < 0) {
                        start = index;
                        break;
                    }
                    if (index == nextUsedIndex) {
                        start = this.usedSegments.nextClearBit(index);
                        nextUsedIndex = this.usedSegments.nextSetBit(start + 1);
                        if (nextUsedIndex < 0) {
                            break;
                        }
                    }
                    else {
                        start = index;
                    }
                }
                for (int i = count - 1; i >= 0; --i) {
                    stamps[i] = this.getSegmentLock(start + i).tryWriteLock();
                    if (stamps[i] == 0L) {
                        for (int j = count - 1; j > i; --j) {
                            this.getSegmentLock(start + j).unlockWrite(stamps[j]);
                        }
                        index = start + i + 1;
                        continue Label_0006;
                    }
                }
                return new SegmentRangeWriteLock(start, count, stamps);
            }
            finally {
                this.usedSegmentsLock.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);
        }
    }
    
    protected long segmentsBase() {
        return IndexedStorageFile.HEADER_LENGTH + this.blobCount * 4L;
    }
    
    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 int requiredSegments(final long dataLength) {
        return (int)((dataLength + this.segmentSize - 1L) / this.segmentSize);
    }
    
    public FileLock lock() throws IOException {
        return this.fileChannel.lock();
    }
    
    public void force(final boolean metaData) throws IOException {
        this.fileChannel.force(metaData);
        this.mappedBlobIndexes.force();
    }
    
    @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 + ", blobCount=" + this.blobCount + ", segmentSize=" + this.segmentSize + ", mappedBlobIndexes=" + String.valueOf(this.mappedBlobIndexes) + ", usedSegments=" + String.valueOf(this.usedSegments);
    }
    
    static {
        EMPTY_STAMPED_LOCKS = new StampedLock[0];
        METRICS_REGISTRY = new MetricsRegistry<IndexedStorageFile>().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("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.HOH.next(20);
        VERSION_OFFSET = IndexedStorageFile.HOH.next(4);
        BLOB_COUNT_OFFSET = IndexedStorageFile.HOH.next(4);
        SEGMENT_SIZE_OFFSET = IndexedStorageFile.HOH.next(4);
        HEADER_LENGTH = IndexedStorageFile.HOH.length();
        BOH = new OffsetHelper();
        SRC_LENGTH_OFFSET = IndexedStorageFile.BOH.next(4);
        COMPRESSED_LENGTH_OFFSET = IndexedStorageFile.BOH.next(4);
        BLOB_HEADER_LENGTH = IndexedStorageFile.BOH.length();
        NO_ATTRIBUTES = new FileAttribute[0];
        MAGIC_BYTES = "HytaleIndexedStorage".getBytes(StandardCharsets.UTF_8);
        (MAGIC_BUFFER = ByteBuffer.wrap(IndexedStorageFile.MAGIC_BYTES)).position(0);
        CACHED_TEMP_BUFFER = ThreadLocal.withInitial(() -> ByteBuffer.allocateDirect(IndexedStorageFile.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 SegmentRangeWriteLock
    {
        private final int segmentIndex;
        private final int count;
        private final long[] stamps;
        
        public SegmentRangeWriteLock(final int segmentIndex, final int count, final long[] stamps) {
            if (segmentIndex == 0) {
                throw new IllegalArgumentException("Invalid segment index!");
            }
            if (count == 0) {
                throw new IllegalArgumentException("Invalid count!");
            }
            this.segmentIndex = segmentIndex;
            this.count = count;
            this.stamps = stamps;
        }
        
        protected void unlock() {
            for (int i = 0; i < this.count; ++i) {
                IndexedStorageFile.this.getSegmentLock(this.segmentIndex + i).unlockWrite(this.stamps[i]);
                this.stamps[i] = 0L;
            }
        }
    }
}
