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

package com.hypixel.hytale.builtin.asseteditor;

import java.nio.file.FileVisitor;
import com.hypixel.hytale.server.core.asset.common.CommonAssetModule;
import java.nio.file.FileVisitResult;
import java.nio.file.attribute.BasicFileAttributes;
import java.nio.file.SimpleFileVisitor;
import com.hypixel.hytale.server.core.util.io.FileUtil;
import java.nio.file.Files;
import java.nio.file.LinkOption;
import java.util.Set;
import com.hypixel.hytale.builtin.asseteditor.util.AssetPathUtil;
import java.util.HashSet;
import java.util.Comparator;
import java.io.IOException;
import com.hypixel.hytale.common.util.FormatUtil;
import java.util.logging.Level;
import java.util.Iterator;
import com.hypixel.hytale.builtin.asseteditor.data.AssetState;
import com.hypixel.hytale.builtin.asseteditor.data.ModifiedAsset;
import java.util.Map;
import javax.annotation.Nullable;
import com.hypixel.hytale.common.util.ListUtil;
import com.hypixel.hytale.common.util.PathUtil;
import com.hypixel.hytale.protocol.Packet;
import com.hypixel.hytale.protocol.packets.asseteditor.AssetEditorAssetListSetup;
import com.hypixel.hytale.protocol.packets.asseteditor.AssetEditorFileTree;
import javax.annotation.Nonnull;
import com.hypixel.hytale.builtin.asseteditor.assettypehandler.AssetTypeHandler;
import java.util.Collection;
import it.unimi.dsi.fastutil.objects.ObjectArrayList;
import com.hypixel.hytale.protocol.packets.asseteditor.AssetEditorFileEntry;
import java.util.List;
import java.nio.file.Path;
import java.util.concurrent.locks.StampedLock;
import com.hypixel.hytale.logger.HytaleLogger;

public class AssetTree
{
    private static final HytaleLogger LOGGER;
    private final StampedLock lock;
    private final Path rootPath;
    private final String packKey;
    private final boolean isReadOnly;
    private final boolean canBeDeleted;
    List<AssetEditorFileEntry> serverAssets;
    List<AssetEditorFileEntry> commonAssets;
    
    public AssetTree(final Path rootPath, final String packKey, final boolean isReadOnly, final boolean canBeDeleted) {
        this.lock = new StampedLock();
        this.serverAssets = new ObjectArrayList<AssetEditorFileEntry>();
        this.commonAssets = new ObjectArrayList<AssetEditorFileEntry>();
        this.rootPath = rootPath;
        this.packKey = packKey;
        this.isReadOnly = isReadOnly;
        this.canBeDeleted = canBeDeleted;
    }
    
    public AssetTree(final Path rootPath, final String packKey, final boolean isReadOnly, final boolean canBeDeleted, @Nonnull final Collection<AssetTypeHandler> assetTypes) {
        this.lock = new StampedLock();
        this.serverAssets = new ObjectArrayList<AssetEditorFileEntry>();
        this.commonAssets = new ObjectArrayList<AssetEditorFileEntry>();
        this.rootPath = rootPath;
        this.packKey = packKey;
        this.isReadOnly = isReadOnly;
        this.canBeDeleted = canBeDeleted;
        this.load(assetTypes);
    }
    
    public void replaceAssetTree(@Nonnull final AssetTree assetTree) {
        final long stamp = this.lock.writeLock();
        try {
            this.serverAssets = assetTree.serverAssets;
            this.commonAssets = assetTree.commonAssets;
        }
        finally {
            this.lock.unlockWrite(stamp);
        }
    }
    
    public void sendPackets(@Nonnull final EditorClient editorClient) {
        final long stamp = this.lock.readLock();
        try {
            editorClient.getPacketHandler().write(new AssetEditorAssetListSetup(this.packKey, this.isReadOnly, this.canBeDeleted, AssetEditorFileTree.Server, this.serverAssets.toArray(AssetEditorFileEntry[]::new)));
            editorClient.getPacketHandler().write(new AssetEditorAssetListSetup(this.packKey, this.isReadOnly, this.canBeDeleted, AssetEditorFileTree.Common, this.commonAssets.toArray(AssetEditorFileEntry[]::new)));
        }
        finally {
            this.lock.unlockRead(stamp);
        }
    }
    
    public boolean isDirectoryEmpty(@Nonnull final Path path) {
        final String pathString = PathUtil.toUnixPathString(path);
        final long stamp = this.lock.readLock();
        try {
            final List<AssetEditorFileEntry> assets = this.getAssetListForPath(path);
            final int index = ListUtil.binarySearch(assets, o -> o.path, pathString, String::compareTo);
            if (index < 0) {
                return true;
            }
            if (!assets.get(index).isDirectory) {
                return false;
            }
            final int fileIndex = index + 1;
            if (fileIndex >= assets.size()) {
                return false;
            }
            final boolean hasFile = assets.get(fileIndex).path.startsWith(pathString);
            return !hasFile;
        }
        finally {
            this.lock.unlockRead(stamp);
        }
    }
    
    @Nullable
    public AssetEditorFileEntry ensureAsset(@Nonnull final Path path, final boolean isDirectory) {
        final String pathString = PathUtil.toUnixPathString(path);
        final long stamp = this.lock.writeLock();
        try {
            final List<AssetEditorFileEntry> assets = this.getAssetListForPath(path);
            final int index = ListUtil.binarySearch(assets, o -> o.path, pathString, String::compareTo);
            if (index >= 0) {
                return null;
            }
            int insertionPoint = -(index + 1);
            if (path.getNameCount() > 1) {
                Path parentPath = path.getName(0);
                for (int i = 1; i < path.getNameCount() - 1; ++i) {
                    parentPath = parentPath.resolve(path.getName(i));
                    final String name = PathUtil.toUnixPathString(parentPath);
                    if (insertionPoint <= 0 || !assets.get(insertionPoint - 1).path.startsWith(name)) {
                        assets.add(insertionPoint++, new AssetEditorFileEntry(name, true));
                    }
                }
            }
            final AssetEditorFileEntry entry = new AssetEditorFileEntry(pathString, isDirectory);
            assets.add(insertionPoint, entry);
            return entry;
        }
        finally {
            this.lock.unlockWrite(stamp);
        }
    }
    
    @Nullable
    public AssetEditorFileEntry getAssetFile(@Nonnull final Path path) {
        final String pathString = PathUtil.toUnixPathString(path);
        final long stamp = this.lock.readLock();
        try {
            final List<AssetEditorFileEntry> assets = this.getAssetListForPath(path);
            final int index = ListUtil.binarySearch(assets, o -> o.path, pathString, String::compareTo);
            return (index >= 0) ? assets.get(index) : null;
        }
        finally {
            this.lock.unlockRead(stamp);
        }
    }
    
    @Nullable
    public AssetEditorFileEntry removeAsset(@Nonnull final Path path) {
        final String pathString = PathUtil.toUnixPathString(path);
        final long stamp = this.lock.writeLock();
        try {
            final List<AssetEditorFileEntry> assets = this.getAssetListForPath(path);
            final int index = ListUtil.binarySearch(assets, o -> o.path, pathString, String::compareTo);
            if (index < 0) {
                return null;
            }
            final AssetEditorFileEntry entry = assets.remove(index);
            if (entry.isDirectory) {
                final String pathPrefix = pathString;
                int removeCount = 0;
                for (int i = index; i < assets.size(); ++i) {
                    final AssetEditorFileEntry asset = assets.get(i);
                    if (!asset.path.startsWith(pathPrefix)) {
                        break;
                    }
                    ++removeCount;
                }
                for (int i = 0; i < removeCount; ++i) {
                    assets.remove(index);
                }
            }
            return entry;
        }
        finally {
            this.lock.unlockWrite(stamp);
        }
    }
    
    public void applyAssetChanges(@Nonnull final Map<Path, ModifiedAsset> createdDirectories, @Nonnull final Map<Path, ModifiedAsset> modifiedAssets) {
        for (final ModifiedAsset dir : createdDirectories.values()) {
            this.ensureAsset(dir.path, true);
        }
        for (final ModifiedAsset file : modifiedAssets.values()) {
            if (file.state == AssetState.NEW) {
                this.ensureAsset(file.path, false);
            }
            else if (file.state == AssetState.DELETED) {
                this.removeAsset((file.oldPath != null) ? file.oldPath : file.path);
            }
            else {
                if (file.oldPath == null) {
                    continue;
                }
                this.removeAsset(file.oldPath);
                this.ensureAsset(file.path, false);
            }
        }
    }
    
    private List<AssetEditorFileEntry> getAssetListForPath(@Nonnull final Path path) {
        if (path.getNameCount() > 0) {
            String firstName = path.getName(0).toString();
            if (firstName.equals("..")) {
                try {
                    firstName = path.getName(2).toString();
                }
                catch (final IllegalArgumentException ex) {}
            }
            if ("Server".equals(firstName)) {
                return this.serverAssets;
            }
            if ("Common".equals(firstName)) {
                return this.commonAssets;
            }
        }
        throw new IllegalArgumentException("Invalid path " + String.valueOf(path));
    }
    
    private void load(@Nonnull final Collection<AssetTypeHandler> assetTypes) {
        try {
            final long start = System.nanoTime();
            loadServerAssets(this.rootPath, assetTypes, this.serverAssets);
            AssetTree.LOGGER.at(Level.INFO).log("Loaded Server/ asset tree! Took: %s", FormatUtil.nanosToString(System.nanoTime() - start));
        }
        catch (final IOException e) {
            AssetTree.LOGGER.at(Level.WARNING).withCause(e).log("Failed to load server asset tree!");
        }
        try {
            final long start = System.nanoTime();
            walkFileTree(this.rootPath, this.rootPath.resolve("Common"), this.commonAssets);
            AssetTree.LOGGER.at(Level.INFO).log("Loaded Common/ asset tree! Took: %s", FormatUtil.nanosToString(System.nanoTime() - start));
        }
        catch (final IOException e) {
            AssetTree.LOGGER.at(Level.WARNING).withCause(e).log("Failed to load common asset tree!");
        }
        final long start = System.nanoTime();
        this.serverAssets.sort(Comparator.comparing(o -> o.path));
        this.commonAssets.sort(Comparator.comparing(o -> o.path));
        AssetTree.LOGGER.at(Level.INFO).log("Sorted asset tree! Took: %s", FormatUtil.nanosToString(System.nanoTime() - start));
    }
    
    private static void loadServerAssets(@Nonnull final Path root, @Nonnull final Collection<AssetTypeHandler> assetTypes, @Nonnull final List<AssetEditorFileEntry> files) throws IOException {
        final Set<String> assetTypePaths = new HashSet<String>();
        final Set<String> subPaths = new HashSet<String>();
        for (final AssetTypeHandler assetTypeHandler : assetTypes) {
            if (!assetTypeHandler.getRootPath().startsWith(AssetPathUtil.PATH_DIR_SERVER)) {
                continue;
            }
            final String assetTypePath = assetTypeHandler.getConfig().path;
            if (!assetTypePaths.add(assetTypePath)) {
                continue;
            }
            final Path path = Path.of(assetTypePath, new String[0]);
            Path subpath = AssetPathUtil.PATH_DIR_SERVER;
            for (int i = 1; i < path.getNameCount() - 1; ++i) {
                subpath = subpath.resolve(path.getName(i));
                final String name = PathUtil.toUnixPathString(subpath);
                if (subPaths.add(name)) {
                    files.add(new AssetEditorFileEntry(name, true));
                }
            }
        }
        for (final String path2 : assetTypePaths) {
            final Path dirPath = root.resolve(path2);
            walkFileTree(root, dirPath, files);
            final String name2 = PathUtil.toUnixPathString(root.relativize(dirPath));
            files.add(new AssetEditorFileEntry(name2, true));
        }
    }
    
    private static void walkFileTree(@Nonnull final Path root, @Nonnull final Path dirPath, @Nonnull final List<AssetEditorFileEntry> files) throws IOException {
        if (!Files.isDirectory(dirPath, new LinkOption[0])) {
            return;
        }
        Files.walkFileTree(dirPath, FileUtil.DEFAULT_WALK_TREE_OPTIONS_SET, Integer.MAX_VALUE, new SimpleFileVisitor<Path>() {
            @Override
            public FileVisitResult preVisitDirectory(final Path path, final BasicFileAttributes attrs) throws IOException {
                if (path.equals(dirPath)) {
                    return FileVisitResult.CONTINUE;
                }
                files.add(new AssetEditorFileEntry(PathUtil.toUnixPathString(root.relativize(path)), true));
                return super.preVisitDirectory(path, attrs);
            }
            
            @Nonnull
            @Override
            public FileVisitResult visitFile(@Nonnull final Path path, @Nonnull final BasicFileAttributes attrs) {
                if (CommonAssetModule.IGNORED_FILES.contains(path.getFileName())) {
                    return FileVisitResult.CONTINUE;
                }
                files.add(new AssetEditorFileEntry(PathUtil.toUnixPathString(root.relativize(path)), false));
                return FileVisitResult.CONTINUE;
            }
        });
    }
    
    static {
        LOGGER = HytaleLogger.forEnclosingClass();
    }
}
