package com.hypixel.hytale.server.core.universe.world.storage.provider;

import com.hypixel.fastutil.longs.Long2ObjectConcurrentHashMap;
import com.hypixel.hytale.codec.Codec;
import com.hypixel.hytale.codec.KeyedCodec;
import com.hypixel.hytale.codec.builder.BuilderCodec;
import com.hypixel.hytale.codec.codecs.array.ArrayCodec;
import com.hypixel.hytale.component.Resource;
import com.hypixel.hytale.component.ResourceType;
import com.hypixel.hytale.component.Store;
import com.hypixel.hytale.component.SystemGroup;
import com.hypixel.hytale.component.system.StoreSystem;
import com.hypixel.hytale.logger.HytaleLogger;
import com.hypixel.hytale.math.util.ChunkUtil;
import com.hypixel.hytale.metrics.MetricProvider;
import com.hypixel.hytale.metrics.MetricResults;
import com.hypixel.hytale.metrics.MetricsRegistry;
import com.hypixel.hytale.server.core.universe.Universe;
import com.hypixel.hytale.server.core.universe.world.World;
import com.hypixel.hytale.server.core.universe.world.storage.BufferChunkLoader;
import com.hypixel.hytale.server.core.universe.world.storage.BufferChunkSaver;
import com.hypixel.hytale.server.core.universe.world.storage.ChunkStore;
import com.hypixel.hytale.server.core.universe.world.storage.IChunkLoader;
import com.hypixel.hytale.server.core.universe.world.storage.IChunkSaver;
import com.hypixel.hytale.sneakythrow.SneakyThrow;
import com.hypixel.hytale.storage.IndexedStorageFile;
import it.unimi.dsi.fastutil.ints.IntList;
import it.unimi.dsi.fastutil.ints.IntListIterator;
import it.unimi.dsi.fastutil.longs.Long2ObjectMap.Entry;
import it.unimi.dsi.fastutil.longs.LongOpenHashSet;
import it.unimi.dsi.fastutil.longs.LongSet;
import it.unimi.dsi.fastutil.longs.LongSets;

import javax.annotation.Nonnull;
import javax.annotation.Nullable;
import java.io.Closeable;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.nio.ByteBuffer;
import java.nio.file.FileAlreadyExistsException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.StandardCopyOption;
import java.nio.file.StandardOpenOption;
import java.util.Iterator;
import java.util.Map;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.Executors;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.logging.Level;
import java.util.stream.Stream;

public class IndexedStorageChunkStorageProvider implements IChunkStorageProvider {
    public static final String ID = "IndexedStorage";
    @Nonnull
    public static final BuilderCodec<IndexedStorageChunkStorageProvider> CODEC = BuilderCodec.builder(
                    IndexedStorageChunkStorageProvider.class, IndexedStorageChunkStorageProvider::new
            )
            .documentation("Uses the indexed storage file format to store chunks.")
            .<Boolean>appendInherited(
                    new KeyedCodec<>("FlushOnWrite", Codec.BOOLEAN), (o, i) -> o.flushOnWrite = i, o -> o.flushOnWrite, (o, p) -> o.flushOnWrite = p.flushOnWrite
            )
            .documentation(
                    "Controls whether the indexed storage flushes during writes.\nRecommended to be enabled to prevent corruption of chunks during unclean shutdowns."
            )
            .add()
            .build();
    private boolean flushOnWrite = false;

    public IndexedStorageChunkStorageProvider() {
    }

    @Nonnull
    @Override
    public IChunkLoader getLoader(@Nonnull Store<ChunkStore> store) {
        return new IndexedStorageChunkStorageProvider.IndexedStorageChunkLoader(store, this.flushOnWrite);
    }

    @Nonnull
    @Override
    public IChunkSaver getSaver(@Nonnull Store<ChunkStore> store) {
        return new IndexedStorageChunkStorageProvider.IndexedStorageChunkSaver(store, this.flushOnWrite);
    }

    @Nonnull
    @Override
    public String toString() {
        return "IndexedStorageChunkStorageProvider{}";
    }

    @Nonnull
    private static String toFileName(int regionX, int regionZ) {
        return regionX + "." + regionZ + ".region.bin";
    }

    private static long fromFileName(@Nonnull String fileName) {
        String[] split = fileName.split("\\.");
        if (split.length != 4) {
            throw new IllegalArgumentException("Unexpected file name format!");
        } else if (!"region".equals(split[2])) {
            throw new IllegalArgumentException("Unexpected file name format!");
        } else if (!"bin".equals(split[3])) {
            throw new IllegalArgumentException("Unexpected file extension!");
        } else {
            int regionX = Integer.parseInt(split[0]);
            int regionZ = Integer.parseInt(split[1]);
            return ChunkUtil.indexChunk(regionX, regionZ);
        }
    }

    public static class IndexedStorageCache implements Closeable, MetricProvider, Resource<ChunkStore> {
        @Nonnull
        public static final MetricsRegistry<IndexedStorageChunkStorageProvider.IndexedStorageCache> METRICS_REGISTRY = new MetricsRegistry<IndexedStorageChunkStorageProvider.IndexedStorageCache>()
                .register(
                        "Files",
                        cache -> cache.cache
                                .long2ObjectEntrySet()
                                .stream()
                                .map(IndexedStorageChunkStorageProvider.IndexedStorageCache.CacheEntryMetricData::new)
                                .toArray(IndexedStorageChunkStorageProvider.IndexedStorageCache.CacheEntryMetricData[]::new),
                        new ArrayCodec<>(
                                IndexedStorageChunkStorageProvider.IndexedStorageCache.CacheEntryMetricData.CODEC,
                                IndexedStorageChunkStorageProvider.IndexedStorageCache.CacheEntryMetricData[]::new
                        )
                );

        private static final HytaleLogger LOGGER = HytaleLogger.forEnclosingClass();

        /**
         * How long a region file can be idle before being closed (in milliseconds)
         */
        private static final long IDLE_TIMEOUT_MS = TimeUnit.MINUTES.toMillis(5);
        /**
         * How often to check for idle region files (in milliseconds)
         */
        private static final long CLEANUP_INTERVAL_MS = TimeUnit.MINUTES.toMillis(1);

        /**
         * Wrapper for IndexedStorageFile that tracks usage for async-safe cleanup.
         */
        private static class CachedFile {
            final IndexedStorageFile file;
            final AtomicInteger activeOps = new AtomicInteger(0);
            volatile long lastAccessTime;
            volatile boolean markedForClose = false;

            CachedFile(IndexedStorageFile file) {
                this.file = file;
                this.lastAccessTime = System.currentTimeMillis();
            }

            void recordAccess() {
                this.lastAccessTime = System.currentTimeMillis();
            }

            /**
             * Acquire a reference for an operation. Returns false if file is marked for close.
             */
            boolean acquire() {
                if (markedForClose) {
                    return false;
                }
                activeOps.incrementAndGet();
                // Double-check after increment
                if (markedForClose) {
                    activeOps.decrementAndGet();
                    return false;
                }
                recordAccess();
                return true;
            }

            void release() {
                activeOps.decrementAndGet();
            }

            boolean isIdle(long threshold) {
                return lastAccessTime < threshold && activeOps.get() == 0;
            }
        }

        private final Long2ObjectConcurrentHashMap<IndexedStorageFile> cache = new Long2ObjectConcurrentHashMap<>(true, ChunkUtil.NOT_FOUND);
        private final Map<Long, CachedFile> trackedFiles = new ConcurrentHashMap<>();
        private Path path;
        private ScheduledExecutorService cleanupExecutor;

        public IndexedStorageCache() {
        }

        private void startCleanupTask() {
            if (cleanupExecutor != null) {
                return;
            }
            LOGGER.at(Level.INFO).log("Starting region file cleanup task (idle timeout: %ds, interval: %ds)",
                    IDLE_TIMEOUT_MS / 1000, CLEANUP_INTERVAL_MS / 1000);
            cleanupExecutor = Executors.newSingleThreadScheduledExecutor(r -> {
                Thread t = new Thread(r, "IndexedStorageCache-Cleanup");
                t.setDaemon(true);
                return t;
            });
            cleanupExecutor.scheduleAtFixedRate(() -> {
                try {
                    cleanupIdleFiles();
                } catch (Exception e) {
                    LOGGER.at(Level.SEVERE).withCause(e).log("Region cleanup task failed");
                }
            }, CLEANUP_INTERVAL_MS, CLEANUP_INTERVAL_MS, TimeUnit.MILLISECONDS);
        }

        private void cleanupIdleFiles() {
            long now = System.currentTimeMillis();
            long threshold = now - IDLE_TIMEOUT_MS;
            int totalFiles = trackedFiles.size();
            int idleCount = 0;

            LOGGER.at(Level.INFO).log("Running region cleanup check: %d tracked files", totalFiles);

            for (Map.Entry<Long, CachedFile> entry : trackedFiles.entrySet()) {
                long regionKey = entry.getKey();
                CachedFile cached = entry.getValue();

                long idleTimeMs = now - cached.lastAccessTime;
                int activeOps = cached.activeOps.get();

                if (cached.isIdle(threshold)) {
                    idleCount++;
                    // Mark for close first - prevents new acquisitions
                    cached.markedForClose = true;

                    // Double-check no operations started between isIdle check and marking
                    if (cached.activeOps.get() > 0) {
                        cached.markedForClose = false;
                        LOGGER.at(Level.INFO).log("Region cleanup skipped - ops started during close");
                        continue;
                    }

                    // Safe to close now
                    cache.remove(regionKey);
                    trackedFiles.remove(regionKey);

                    int regionX = ChunkUtil.xOfChunkIndex(regionKey);
                    int regionZ = ChunkUtil.zOfChunkIndex(regionKey);
                    try {
                        cached.file.close();
                        LOGGER.at(Level.INFO).log("%d,%d region unloaded", regionX, regionZ);
                    } catch (IOException e) {
                        LOGGER.at(Level.WARNING).withCause(e).log("Failed to close idle region file %d.%d", regionX, regionZ);
                    }
                } else {
                    LOGGER.at(Level.FINE).log("Region %d not idle: lastAccess=%dms ago, activeOps=%d, threshold=%dms",
                            regionKey, idleTimeMs, activeOps, IDLE_TIMEOUT_MS);
                }
            }

            if (totalFiles > 0) {
                LOGGER.at(Level.INFO).log("Region cleanup complete: %d/%d files were idle", idleCount, totalFiles);
            }
        }

        private void trackFile(long regionKey, IndexedStorageFile file) {
            trackedFiles.computeIfAbsent(regionKey, k -> new CachedFile(file));
        }

        private void recordAccess(long regionKey) {
            CachedFile cached = trackedFiles.get(regionKey);
            if (cached != null) {
                cached.recordAccess();
            }
        }

        /**
         * Acquire a file for use. Must call releaseFile() when done.
         * Returns null if file doesn't exist or is being closed.
         */
        @Nullable
        public IndexedStorageFile acquireFile(int regionX, int regionZ, boolean flushOnWrite) {
            long regionKey = ChunkUtil.indexChunk(regionX, regionZ);
            IndexedStorageFile file = getOrTryOpen(regionX, regionZ, flushOnWrite);
            if (file == null) {
                return null;
            }

            CachedFile cached = trackedFiles.get(regionKey);
            if (cached != null && cached.acquire()) {
                return file;
            }

            // File is being closed, retry to get a fresh one
            return getOrTryOpen(regionX, regionZ, flushOnWrite);
        }

        /**
         * Release a file after use.
         */
        public void releaseFile(int regionX, int regionZ) {
            long regionKey = ChunkUtil.indexChunk(regionX, regionZ);
            CachedFile cached = trackedFiles.get(regionKey);
            if (cached != null) {
                cached.release();
            }
        }

        public static ResourceType<ChunkStore, IndexedStorageChunkStorageProvider.IndexedStorageCache> getResourceType() {
            return Universe.get().getIndexedStorageCacheResourceType();
        }

        @Nonnull
        public Long2ObjectConcurrentHashMap<IndexedStorageFile> getCache() {
            return this.cache;
        }

        @Override
        public void close() throws IOException {
            // Shutdown cleanup task first
            if (cleanupExecutor != null) {
                cleanupExecutor.shutdown();
                try {
                    cleanupExecutor.awaitTermination(5, TimeUnit.SECONDS);
                } catch (InterruptedException e) {
                    Thread.currentThread().interrupt();
                }
                cleanupExecutor = null;
            }

            IOException exception = null;
            Iterator<IndexedStorageFile> iterator = this.cache.values().iterator();

            while (iterator.hasNext()) {
                try {
                    iterator.next().close();
                    iterator.remove();
                } catch (Exception var4) {
                    if (exception == null) {
                        exception = new IOException("Failed to close one or more loaders!");
                    }

                    exception.addSuppressed(var4);
                }
            }

            trackedFiles.clear();

            if (exception != null) {
                throw exception;
            }
        }

        @Nullable
        public IndexedStorageFile getOrTryOpen(int regionX, int regionZ, boolean flushOnWrite) {
            long regionKey = ChunkUtil.indexChunk(regionX, regionZ);
            IndexedStorageFile file = this.cache.computeIfAbsent(regionKey, k -> {
                startCleanupTask();
                Path regionFile = this.path.resolve(IndexedStorageChunkStorageProvider.toFileName(regionX, regionZ));
                if (!Files.exists(regionFile)) {
                    return null;
                } else {
                    try {
                        IndexedStorageFile open = IndexedStorageFile.open(regionFile, StandardOpenOption.READ, StandardOpenOption.WRITE);
                        open.setFlushOnWrite(flushOnWrite);
                        return open;
                    } catch (FileNotFoundException var8) {
                        return null;
                    } catch (Exception var9) {
                        // Corruption detected - rename file and return null to trigger regeneration
                        LOGGER.at(Level.SEVERE).withCause(var9).log("Corrupted region file detected: %s", regionFile);
                        try {
                            Path corruptedPath = regionFile.resolveSibling(regionFile.getFileName() + ".corrupted");
                            Files.move(regionFile, corruptedPath, StandardCopyOption.REPLACE_EXISTING);
                            LOGGER.at(Level.WARNING).log("Renamed corrupted file to: %s", corruptedPath);
                        } catch (IOException moveErr) {
                            LOGGER.at(Level.SEVERE).withCause(moveErr).log("Failed to rename corrupted file: %s", regionFile);
                        }
                        return null;
                    }
                }
            });
            if (file != null) {
                trackFile(regionKey, file);
                recordAccess(regionKey);
            }
            return file;
        }

        /**
         * Marks a region file as corrupted by renaming it with .corrupted suffix.
         * The file will be regenerated on next access.
         */
        public void markRegionCorrupted(int regionX, int regionZ) {
            long key = ChunkUtil.indexChunk(regionX, regionZ);
            IndexedStorageFile file = this.cache.remove(key);
            CachedFile cached = trackedFiles.remove(key);
            if (cached != null) {
                cached.markedForClose = true;
            }
            if (file != null) {
                try {
                    file.close();
                } catch (IOException e) {
                    // Ignore close errors
                }
            }
            Path regionFile = this.path.resolve(IndexedStorageChunkStorageProvider.toFileName(regionX, regionZ));
            if (Files.exists(regionFile)) {
                try {
                    Path corruptedPath = regionFile.resolveSibling(regionFile.getFileName() + ".corrupted");
                    Files.move(regionFile, corruptedPath, StandardCopyOption.REPLACE_EXISTING);
                    LOGGER.at(Level.WARNING).log("Marked region %d.%d as corrupted: %s", regionX, regionZ, corruptedPath);
                } catch (IOException e) {
                    LOGGER.at(Level.SEVERE).withCause(e).log("Failed to mark region %d.%d as corrupted", regionX, regionZ);
                }
            }
        }

        @Nonnull
        public IndexedStorageFile getOrCreate(int regionX, int regionZ, boolean flushOnWrite) {
            long regionKey = ChunkUtil.indexChunk(regionX, regionZ);
            IndexedStorageFile file = this.cache.computeIfAbsent(regionKey, k -> {
                startCleanupTask();
                try {
                    if (!Files.exists(this.path)) {
                        try {
                            Files.createDirectory(this.path);
                        } catch (FileAlreadyExistsException var8) {
                        }
                    }

                    Path regionFile = this.path.resolve(IndexedStorageChunkStorageProvider.toFileName(regionX, regionZ));
                    IndexedStorageFile open = IndexedStorageFile.open(regionFile, StandardOpenOption.CREATE, StandardOpenOption.READ, StandardOpenOption.WRITE);
                    open.setFlushOnWrite(flushOnWrite);
                    return open;
                } catch (IOException var9) {
                    throw SneakyThrow.sneakyThrow(var9);
                }
            });
            trackFile(regionKey, file);
            recordAccess(regionKey);
            return file;
        }

        @Nonnull
        public LongSet getIndexes() throws IOException {
            if (!Files.exists(this.path)) {
                return LongSets.EMPTY_SET;
            } else {
                LongOpenHashSet chunkIndexes = new LongOpenHashSet();

                try (Stream<Path> stream = Files.list(this.path)) {
                    stream.forEach(path -> {
                        if (!Files.isDirectory(path)) {
                            long regionIndex;
                            try {
                                regionIndex = IndexedStorageChunkStorageProvider.fromFileName(path.getFileName().toString());
                            } catch (IllegalArgumentException var15) {
                                return;
                            }

                            int regionX = ChunkUtil.xOfChunkIndex(regionIndex);
                            int regionZ = ChunkUtil.zOfChunkIndex(regionIndex);
                            IndexedStorageFile regionFile = this.getOrTryOpen(regionX, regionZ, true);
                            if (regionFile != null) {
                                IntList blobIndexes = regionFile.keys();
                                IntListIterator iterator = blobIndexes.iterator();

                                while (iterator.hasNext()) {
                                    int blobIndex = iterator.nextInt();
                                    int localX = ChunkUtil.xFromColumn(blobIndex);
                                    int localZ = ChunkUtil.zFromColumn(blobIndex);
                                    int chunkX = regionX << 5 | localX;
                                    int chunkZ = regionZ << 5 | localZ;
                                    chunkIndexes.add(ChunkUtil.indexChunk(chunkX, chunkZ));
                                }
                            }
                        }
                    });
                }

                return chunkIndexes;
            }
        }

        public void flush() throws IOException {
            IOException exception = null;

            for (IndexedStorageFile indexedStorageFile : this.cache.values()) {
                try {
                    indexedStorageFile.force(false);
                } catch (Exception var5) {
                    if (exception == null) {
                        exception = new IOException("Failed to close one or more loaders!");
                    }

                    exception.addSuppressed(var5);
                }
            }

            if (exception != null) {
                throw exception;
            }
        }

        @Nonnull
        @Override
        public MetricResults toMetricResults() {
            return METRICS_REGISTRY.toMetricResults(this);
        }

        @Nonnull
        @Override
        public Resource<ChunkStore> clone() {
            return new IndexedStorageChunkStorageProvider.IndexedStorageCache();
        }

        private static class CacheEntryMetricData {
            @Nonnull
            private static final Codec<IndexedStorageChunkStorageProvider.IndexedStorageCache.CacheEntryMetricData> CODEC = BuilderCodec.builder(
                            IndexedStorageChunkStorageProvider.IndexedStorageCache.CacheEntryMetricData.class,
                            IndexedStorageChunkStorageProvider.IndexedStorageCache.CacheEntryMetricData::new
                    )
                    .append(new KeyedCodec<>("Key", Codec.LONG), (entry, o) -> entry.key = o, entry -> entry.key)
                    .add()
                    .append(new KeyedCodec<>("File", IndexedStorageFile.METRICS_REGISTRY), (entry, o) -> entry.value = o, entry -> entry.value)
                    .add()
                    .build();
            private long key;
            private IndexedStorageFile value;

            public CacheEntryMetricData() {
            }

            public CacheEntryMetricData(@Nonnull Entry<IndexedStorageFile> entry) {
                this.key = entry.getLongKey();
                this.value = entry.getValue();
            }
        }
    }

    public static class IndexedStorageCacheSetupSystem extends StoreSystem<ChunkStore> {
        public IndexedStorageCacheSetupSystem() {
        }

        @Nullable
        @Override
        public SystemGroup<ChunkStore> getGroup() {
            return ChunkStore.INIT_GROUP;
        }

        @Override
        public void onSystemAddedToStore(@Nonnull Store<ChunkStore> store) {
            World world = store.getExternalData().getWorld();
            store.getResource(IndexedStorageChunkStorageProvider.IndexedStorageCache.getResourceType()).path = world.getSavePath().resolve("chunks");
        }

        @Override
        public void onSystemRemovedFromStore(@Nonnull Store<ChunkStore> store) {
        }
    }

    public static class IndexedStorageChunkLoader extends BufferChunkLoader implements MetricProvider {
        private final boolean flushOnWrite;

        public IndexedStorageChunkLoader(@Nonnull Store<ChunkStore> store, boolean flushOnWrite) {
            super(store);
            this.flushOnWrite = flushOnWrite;
        }

        @Override
        public void close() throws IOException {
            this.getStore().getResource(IndexedStorageChunkStorageProvider.IndexedStorageCache.getResourceType()).close();
        }

        @Nonnull
        @Override
        public CompletableFuture<ByteBuffer> loadBuffer(int x, int z) {
            int regionX = x >> 5;
            int regionZ = z >> 5;
            int localX = x & 31;
            int localZ = z & 31;
            int index = ChunkUtil.indexColumn(localX, localZ);
            IndexedStorageChunkStorageProvider.IndexedStorageCache indexedStorageCache = this.getStore()
                    .getResource(IndexedStorageChunkStorageProvider.IndexedStorageCache.getResourceType());
            return CompletableFuture.supplyAsync(SneakyThrow.sneakySupplier(() -> {
                IndexedStorageFile chunks = indexedStorageCache.getOrTryOpen(regionX, regionZ, this.flushOnWrite);
                return chunks == null ? null : chunks.readBlob(index);
            }));
        }

        @Nonnull
        @Override
        public LongSet getIndexes() throws IOException {
            return this.getStore().getResource(IndexedStorageChunkStorageProvider.IndexedStorageCache.getResourceType()).getIndexes();
        }

        @Nullable
        @Override
        public MetricResults toMetricResults() {
            return this.getStore().getExternalData().getSaver() instanceof IndexedStorageChunkStorageProvider.IndexedStorageChunkSaver
                    ? null
                    : this.getStore().getResource(IndexedStorageChunkStorageProvider.IndexedStorageCache.getResourceType()).toMetricResults();
        }
    }

    public static class IndexedStorageChunkSaver extends BufferChunkSaver implements MetricProvider {
        private final boolean flushOnWrite;

        protected IndexedStorageChunkSaver(@Nonnull Store<ChunkStore> store, boolean flushOnWrite) {
            super(store);
            this.flushOnWrite = flushOnWrite;
        }

        @Override
        public void close() throws IOException {
            IndexedStorageChunkStorageProvider.IndexedStorageCache indexedStorageCache = this.getStore()
                    .getResource(IndexedStorageChunkStorageProvider.IndexedStorageCache.getResourceType());
            indexedStorageCache.close();
        }

        @Nonnull
        @Override
        public CompletableFuture<Void> saveBuffer(int x, int z, @Nonnull ByteBuffer buffer) {
            int regionX = x >> 5;
            int regionZ = z >> 5;
            int localX = x & 31;
            int localZ = z & 31;
            int index = ChunkUtil.indexColumn(localX, localZ);
            IndexedStorageChunkStorageProvider.IndexedStorageCache indexedStorageCache = this.getStore()
                    .getResource(IndexedStorageChunkStorageProvider.IndexedStorageCache.getResourceType());
            return CompletableFuture.runAsync(SneakyThrow.sneakyRunnable(() -> {
                IndexedStorageFile chunks = indexedStorageCache.getOrCreate(regionX, regionZ, this.flushOnWrite);
                chunks.writeBlob(index, buffer);
            }));
        }

        @Nonnull
        @Override
        public CompletableFuture<Void> removeBuffer(int x, int z) {
            int regionX = x >> 5;
            int regionZ = z >> 5;
            int localX = x & 31;
            int localZ = z & 31;
            int index = ChunkUtil.indexColumn(localX, localZ);
            IndexedStorageChunkStorageProvider.IndexedStorageCache indexedStorageCache = this.getStore()
                    .getResource(IndexedStorageChunkStorageProvider.IndexedStorageCache.getResourceType());
            return CompletableFuture.runAsync(SneakyThrow.sneakyRunnable(() -> {
                IndexedStorageFile chunks = indexedStorageCache.getOrTryOpen(regionX, regionZ, this.flushOnWrite);
                if (chunks != null) {
                    chunks.removeBlob(index);
                }
            }));
        }

        @Nonnull
        @Override
        public CompletableFuture<Void> deleteRegionFile(int x, int z) {
            int regionX = x >> 5;
            int regionZ = z >> 5;
            IndexedStorageChunkStorageProvider.IndexedStorageCache indexedStorageCache = this.getStore()
                    .getResource(IndexedStorageChunkStorageProvider.IndexedStorageCache.getResourceType());
            return CompletableFuture.runAsync(() -> {
                indexedStorageCache.markRegionCorrupted(regionX, regionZ);
            });
        }

        @Nonnull
        @Override
        public LongSet getIndexes() throws IOException {
            return this.getStore().getResource(IndexedStorageChunkStorageProvider.IndexedStorageCache.getResourceType()).getIndexes();
        }

        @Override
        public void flush() throws IOException {
            this.getStore().getResource(IndexedStorageChunkStorageProvider.IndexedStorageCache.getResourceType()).flush();
        }

        @Override
        public MetricResults toMetricResults() {
            return this.getStore().getResource(IndexedStorageChunkStorageProvider.IndexedStorageCache.getResourceType()).toMetricResults();
        }
    }
}
