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

package com.hypixel.hytale.builtin.asseteditor.datasource;

import java.util.ArrayDeque;
import com.hypixel.hytale.builtin.asseteditor.assettypehandler.AssetTypeHandler;
import java.util.Collection;
import java.time.Instant;
import com.hypixel.hytale.builtin.asseteditor.data.AssetState;
import java.nio.file.StandardOpenOption;
import java.nio.file.OpenOption;
import com.hypixel.hytale.server.core.util.HashUtil;
import java.nio.file.CopyOption;
import java.io.IOException;
import java.nio.file.attribute.FileAttribute;
import com.hypixel.hytale.builtin.asseteditor.EditorClient;
import org.bson.BsonArray;
import com.hypixel.hytale.codec.ExtraInfo;
import org.bson.BsonValue;
import com.hypixel.hytale.server.core.util.BsonUtil;
import org.bson.BsonDocument;
import java.nio.file.Files;
import java.nio.file.LinkOption;
import java.util.concurrent.TimeUnit;
import java.util.logging.Level;
import com.hypixel.hytale.server.core.HytaleServer;
import java.util.Iterator;
import com.hypixel.hytale.server.core.Options;
import com.hypixel.hytale.server.core.plugin.PluginManager;
import java.util.concurrent.ScheduledFuture;
import com.hypixel.hytale.builtin.asseteditor.data.ModifiedAsset;
import java.util.Map;
import java.util.concurrent.atomic.AtomicBoolean;
import com.hypixel.hytale.common.plugin.PluginManifest;
import com.hypixel.hytale.builtin.asseteditor.AssetTree;
import java.util.Deque;
import java.util.concurrent.ConcurrentHashMap;
import java.nio.file.Path;
import com.hypixel.hytale.logger.HytaleLogger;

public class StandardDataSource implements DataSource
{
    private static final HytaleLogger LOGGER;
    private final Path rootPath;
    private final ConcurrentHashMap<Path, Deque<EditorFileSaveInfo>> editorSaves;
    private final AssetTree assetTree;
    private final String packKey;
    private final PluginManifest manifest;
    private final boolean isImmutable;
    private final Path recentModificationsFilePath;
    private final AtomicBoolean indexNeedsSaving;
    private final Map<Path, ModifiedAsset> modifiedAssets;
    private ScheduledFuture<?> saveSchedule;
    private boolean isAssetPackBeDeleteable;
    
    public StandardDataSource(final String packKey, final Path rootPath, final boolean isImmutable, final PluginManifest manifest) {
        this.indexNeedsSaving = new AtomicBoolean();
        this.modifiedAssets = new ConcurrentHashMap<Path, ModifiedAsset>();
        this.rootPath = rootPath;
        this.editorSaves = new ConcurrentHashMap<Path, Deque<EditorFileSaveInfo>>();
        this.packKey = packKey;
        this.isImmutable = isImmutable;
        this.manifest = manifest;
        this.isAssetPackBeDeleteable = (!isImmutable && isInModsDirectory(rootPath));
        this.assetTree = new AssetTree(rootPath, packKey, isImmutable, this.isAssetPackBeDeleteable);
        this.recentModificationsFilePath = Path.of("assetEditor", "recentAssetEdits_" + packKey.replace(':', '-') + ".json");
    }
    
    private static boolean isInModsDirectory(final Path path) {
        if (path.startsWith(PluginManager.MODS_PATH)) {
            return true;
        }
        for (final Path modsPath : Options.getOptionSet().valuesOf(Options.MODS_DIRECTORIES)) {
            if (path.startsWith(modsPath)) {
                return true;
            }
        }
        return false;
    }
    
    @Override
    public void start() {
        this.loadRecentModifications();
        this.saveSchedule = HytaleServer.SCHEDULED_EXECUTOR.scheduleWithFixedDelay(() -> {
            try {
                this.saveRecentModifications();
            }
            catch (final Exception e) {
                StandardDataSource.LOGGER.at(Level.SEVERE).withCause(e).log("Failed to save assets index");
            }
        }, 1L, 1L, TimeUnit.MINUTES);
    }
    
    @Override
    public void shutdown() {
        this.saveSchedule.cancel(false);
        this.saveRecentModifications();
    }
    
    private void loadRecentModifications() {
        Path path = this.recentModificationsFilePath;
        if (!Files.exists(path, new LinkOption[0])) {
            path = path.resolveSibling(String.valueOf(path.getFileName()) + ".bak");
            if (!Files.exists(path, new LinkOption[0])) {
                return;
            }
        }
        final BsonDocument doc = BsonUtil.readDocument(path).join();
        final BsonArray assets = doc.getArray("Assets");
        for (final BsonValue asset : assets) {
            final ModifiedAsset modifiedAsset = ModifiedAsset.CODEC.decode(asset, new ExtraInfo());
            if (modifiedAsset == null) {
                continue;
            }
            this.modifiedAssets.put(modifiedAsset.path, modifiedAsset);
        }
    }
    
    public void saveRecentModifications() {
        if (!this.indexNeedsSaving.getAndSet(false)) {
            return;
        }
        StandardDataSource.LOGGER.at(Level.INFO).log("Saving recent asset modification index...");
        final BsonDocument doc = new BsonDocument();
        final BsonArray assetsArray = new BsonArray();
        for (final Map.Entry<Path, ModifiedAsset> modifiedAsset : this.modifiedAssets.entrySet()) {
            assetsArray.add(ModifiedAsset.CODEC.encode((ModifiedAsset)modifiedAsset.getValue(), new ExtraInfo()));
        }
        doc.append("Assets", assetsArray);
        try {
            BsonUtil.writeDocument(this.recentModificationsFilePath, doc);
        }
        catch (final Exception ex) {
            StandardDataSource.LOGGER.at(Level.SEVERE).withCause(ex).log("Failed to save recent asset modification index...");
            this.indexNeedsSaving.set(true);
        }
    }
    
    public boolean canAssetPackBeDeleted() {
        return this.isAssetPackBeDeleteable;
    }
    
    public Path resolveAbsolutePath(final Path path) {
        return this.rootPath.resolve(path.toString()).toAbsolutePath();
    }
    
    @Override
    public Path getFullPathToAssetData(final Path assetPath) {
        return this.resolveAbsolutePath(assetPath);
    }
    
    @Override
    public AssetTree getAssetTree() {
        return this.assetTree;
    }
    
    @Override
    public boolean isImmutable() {
        return this.isImmutable;
    }
    
    @Override
    public Path getRootPath() {
        return this.rootPath;
    }
    
    @Override
    public PluginManifest getManifest() {
        return this.manifest;
    }
    
    @Override
    public boolean doesDirectoryExist(final Path folderPath) {
        return Files.isDirectory(this.resolveAbsolutePath(folderPath), new LinkOption[0]);
    }
    
    @Override
    public boolean createDirectory(final Path dirPath, final EditorClient editorClient) {
        try {
            Files.createDirectory(this.resolveAbsolutePath(dirPath), (FileAttribute<?>[])new FileAttribute[0]);
        }
        catch (final IOException e) {
            StandardDataSource.LOGGER.at(Level.WARNING).withCause(e).log("Failed to create directory %s", dirPath);
            return false;
        }
        return true;
    }
    
    @Override
    public boolean deleteDirectory(final Path dirPath) {
        try {
            Files.deleteIfExists(this.resolveAbsolutePath(dirPath));
        }
        catch (final IOException e) {
            StandardDataSource.LOGGER.at(Level.WARNING).withCause(e).log("Failed to delete directory %s", dirPath);
            return false;
        }
        return true;
    }
    
    @Override
    public boolean moveDirectory(final Path oldDirPath, final Path newDirPath) {
        try {
            Files.move(this.resolveAbsolutePath(oldDirPath), this.resolveAbsolutePath(newDirPath), new CopyOption[0]);
        }
        catch (final IOException e) {
            StandardDataSource.LOGGER.at(Level.WARNING).withCause(e).log("Failed to move directory %s to %s", oldDirPath, newDirPath);
            return false;
        }
        return true;
    }
    
    @Override
    public boolean doesAssetExist(final Path assetPath) {
        return Files.isRegularFile(this.resolveAbsolutePath(assetPath), new LinkOption[0]);
    }
    
    @Override
    public byte[] getAssetBytes(final Path assetPath) {
        try {
            return Files.readAllBytes(this.resolveAbsolutePath(assetPath));
        }
        catch (final IOException e) {
            StandardDataSource.LOGGER.at(Level.WARNING).withCause(e).log("Failed to read asset %s", assetPath);
            return null;
        }
    }
    
    @Override
    public boolean updateAsset(final Path assetPath, final byte[] bytes, final EditorClient editorClient) {
        final Path path = this.resolveAbsolutePath(assetPath);
        try {
            final String hash = HashUtil.sha256(bytes);
            this.trackEditorFileSave(assetPath, hash);
            Files.write(path, bytes, StandardOpenOption.WRITE, StandardOpenOption.TRUNCATE_EXISTING, StandardOpenOption.WRITE);
            final ModifiedAsset modifiedAsset = new ModifiedAsset();
            modifiedAsset.path = assetPath;
            modifiedAsset.state = AssetState.CHANGED;
            modifiedAsset.markEditedBy(editorClient);
            this.putModifiedAsset(modifiedAsset);
        }
        catch (final IOException e) {
            StandardDataSource.LOGGER.at(Level.WARNING).withCause(e).log("Failed to update asset %s", assetPath);
            return false;
        }
        return true;
    }
    
    @Override
    public boolean createAsset(final Path assetPath, final byte[] bytes, final EditorClient editorClient) {
        final Path path = this.resolveAbsolutePath(assetPath);
        try {
            final String hash = HashUtil.sha256(bytes);
            this.trackEditorFileSave(assetPath, hash);
            Files.createDirectories(path.getParent(), (FileAttribute<?>[])new FileAttribute[0]);
            Files.write(path, bytes, StandardOpenOption.CREATE);
            final ModifiedAsset modifiedAsset = new ModifiedAsset();
            modifiedAsset.path = assetPath;
            modifiedAsset.state = AssetState.NEW;
            modifiedAsset.markEditedBy(editorClient);
            this.putModifiedAsset(modifiedAsset);
        }
        catch (final IOException e) {
            StandardDataSource.LOGGER.at(Level.WARNING).withCause(e).log("Failed to create asset %s", assetPath);
            return false;
        }
        return true;
    }
    
    @Override
    public boolean deleteAsset(final Path assetPath, final EditorClient editorClient) {
        try {
            Files.deleteIfExists(this.resolveAbsolutePath(assetPath));
            final ModifiedAsset modifiedAsset = new ModifiedAsset();
            modifiedAsset.path = assetPath;
            modifiedAsset.state = AssetState.DELETED;
            modifiedAsset.markEditedBy(editorClient);
            this.putModifiedAsset(modifiedAsset);
        }
        catch (final IOException e) {
            StandardDataSource.LOGGER.at(Level.WARNING).withCause(e).log("Failed to delete asset %s", assetPath);
            return false;
        }
        return true;
    }
    
    @Override
    public boolean shouldReloadAssetFromDisk(final Path assetPath) {
        final Deque<EditorFileSaveInfo> fileSaveInfos = this.editorSaves.get(assetPath);
        if (fileSaveInfos == null || fileSaveInfos.isEmpty()) {
            return true;
        }
        final byte[] bytes = this.getAssetBytes(assetPath);
        if (bytes == null) {
            return true;
        }
        final String hash = HashUtil.sha256(bytes);
        final long now = System.currentTimeMillis();
        synchronized (fileSaveInfos) {
            EditorFileSaveInfo m = null;
            fileSaveInfos.removeIf(m -> m.expiryMs <= now);
            final Iterator<EditorFileSaveInfo> iterator = fileSaveInfos.iterator();
            while (iterator.hasNext()) {
                m = iterator.next();
                if (m.hash.equals(hash)) {
                    return false;
                }
            }
        }
        return true;
    }
    
    @Override
    public Instant getLastModificationTimestamp(final Path assetPath) {
        return null;
    }
    
    @Override
    public boolean moveAsset(final Path oldAssetPath, final Path newAssetPath, final EditorClient editorClient) {
        try {
            Files.move(this.resolveAbsolutePath(oldAssetPath), this.resolveAbsolutePath(newAssetPath), new CopyOption[0]);
            final ModifiedAsset modifiedAsset = new ModifiedAsset();
            modifiedAsset.path = newAssetPath;
            modifiedAsset.oldPath = oldAssetPath;
            modifiedAsset.state = AssetState.CHANGED;
            modifiedAsset.markEditedBy(editorClient);
            this.putModifiedAsset(modifiedAsset);
        }
        catch (final IOException e) {
            StandardDataSource.LOGGER.at(Level.WARNING).withCause(e).log("Failed to move asset %s to %s", oldAssetPath, newAssetPath);
            return false;
        }
        return true;
    }
    
    @Override
    public AssetTree loadAssetTree(final Collection<AssetTypeHandler> assetTypes) {
        return new AssetTree(this.rootPath, this.packKey, this.isImmutable, this.isAssetPackBeDeleteable, assetTypes);
    }
    
    public void putModifiedAsset(final ModifiedAsset modifiedAsset) {
        this.modifiedAssets.put(modifiedAsset.path, modifiedAsset);
        if (this.modifiedAssets.size() > 50) {
            ModifiedAsset oldestAsset = null;
            for (final ModifiedAsset asset : this.modifiedAssets.values()) {
                if (oldestAsset == null) {
                    oldestAsset = asset;
                }
                else {
                    if (!asset.lastModificationTimestamp.isBefore(oldestAsset.lastModificationTimestamp)) {
                        continue;
                    }
                    oldestAsset = asset;
                }
            }
            this.modifiedAssets.remove(oldestAsset.path);
        }
        this.indexNeedsSaving.set(true);
    }
    
    public Map<Path, ModifiedAsset> getRecentlyModifiedAssets() {
        return this.modifiedAssets;
    }
    
    private void trackEditorFileSave(final Path path, final String hash) {
        final Deque<EditorFileSaveInfo> fileSaves = this.editorSaves.computeIfAbsent(path, p -> new ArrayDeque());
        synchronized (fileSaves) {
            fileSaves.addLast(new EditorFileSaveInfo(hash, System.currentTimeMillis() + 30000L));
            while (fileSaves.size() > 20) {
                fileSaves.removeFirst();
            }
        }
    }
    
    static {
        LOGGER = HytaleLogger.forEnclosingClass();
    }
    
    record EditorFileSaveInfo(String hash, long expiryMs) {}
}
