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

package com.hypixel.hytale.server.core.asset.common;

import com.hypixel.hytale.event.IEventDispatcher;
import java.util.stream.Stream;
import com.hypixel.hytale.server.core.asset.common.events.CommonAssetMonitorEvent;
import com.hypixel.hytale.server.core.HytaleServer;
import com.hypixel.hytale.common.util.PathUtil;
import java.util.Map;
import com.hypixel.hytale.server.core.asset.monitor.EventKind;
import com.hypixel.hytale.protocol.packets.setup.RemoveAssets;
import com.hypixel.hytale.protocol.ItemWithAllMetadata;
import com.hypixel.hytale.protocol.packets.interface_.Notification;
import com.hypixel.hytale.protocol.packets.setup.WorldLoadProgress;
import com.hypixel.hytale.math.util.MathUtil;
import com.hypixel.hytale.protocol.packets.setup.AssetFinalize;
import com.hypixel.hytale.protocol.packets.setup.AssetPart;
import com.hypixel.hytale.protocol.packets.setup.AssetInitialize;
import com.hypixel.hytale.common.util.ArrayUtil;
import java.util.Objects;
import com.hypixel.hytale.server.core.io.PacketHandler;
import com.hypixel.hytale.logger.sentry.SkipSentryException;
import java.nio.file.NoSuchFileException;
import java.nio.file.FileVisitor;
import java.util.function.Supplier;
import com.hypixel.hytale.common.util.PatternUtil;
import java.nio.file.FileVisitResult;
import java.nio.file.SimpleFileVisitor;
import com.hypixel.hytale.server.core.util.io.FileUtil;
import java.time.temporal.TemporalUnit;
import java.time.temporal.ChronoUnit;
import java.nio.file.attribute.BasicFileAttributes;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.atomic.AtomicInteger;
import java.io.BufferedReader;
import com.hypixel.hytale.server.core.asset.common.asset.FileCommonAsset;
import javax.annotation.Nullable;
import com.hypixel.hytale.server.core.util.NotificationUtil;
import com.hypixel.hytale.protocol.packets.interface_.NotificationStyle;
import com.hypixel.hytale.server.core.Message;
import com.hypixel.hytale.server.core.asset.monitor.AssetMonitor;
import java.io.IOException;
import com.hypixel.hytale.sneakythrow.SneakyThrow;
import com.hypixel.hytale.server.core.asset.monitor.AssetMonitorHandler;
import java.nio.file.Files;
import java.nio.file.LinkOption;
import com.hypixel.hytale.common.util.FormatUtil;
import java.util.logging.Level;
import com.hypixel.hytale.logger.HytaleLogger;
import it.unimi.dsi.fastutil.booleans.BooleanObjectPair;
import java.util.Collection;
import com.hypixel.hytale.protocol.Packet;
import com.hypixel.hytale.protocol.packets.setup.RequestCommonAssetsRebuild;
import com.hypixel.hytale.server.core.universe.Universe;
import it.unimi.dsi.fastutil.objects.ObjectArrayList;
import java.util.Iterator;
import com.hypixel.hytale.server.core.asset.AssetPackUnregisterEvent;
import com.hypixel.hytale.server.core.asset.AssetPackRegisterEvent;
import com.hypixel.hytale.assetstore.AssetPack;
import com.hypixel.hytale.server.core.asset.AssetModule;
import com.hypixel.hytale.server.core.asset.LoadAssetEvent;
import com.hypixel.hytale.server.core.asset.common.events.SendCommonAssetsEvent;
import com.hypixel.hytale.function.supplier.SupplierUtil;
import java.util.function.Function;
import java.util.List;
import javax.annotation.Nonnull;
import com.hypixel.hytale.server.core.plugin.JavaPluginInit;
import com.hypixel.hytale.protocol.Asset;
import com.hypixel.hytale.function.supplier.CachedSupplier;
import java.time.Instant;
import java.nio.file.Path;
import java.util.Set;
import com.hypixel.hytale.common.plugin.PluginManifest;
import com.hypixel.hytale.server.core.plugin.JavaPlugin;

public class CommonAssetModule extends JavaPlugin
{
    public static final PluginManifest MANIFEST;
    private static CommonAssetModule instance;
    public static final Set<Path> IGNORED_FILES;
    public static final Instant TICK_TIMESTAMP_ORIGIN;
    public static final String ASSET_INDEX_VERSION_IDENTIFIER = "VERSION=";
    public static final int ASSET_INDEX_HASHES_VERSION = 0;
    public static final int ASSET_INDEX_CACHE_VERSION = 1;
    public static final int MAX_FRAME = 2621440;
    private final CachedSupplier<Asset[]> assets;
    
    public static CommonAssetModule get() {
        return CommonAssetModule.instance;
    }
    
    public CommonAssetModule(@Nonnull final JavaPluginInit init) {
        super(init);
        this.assets = SupplierUtil.cache(() -> CommonAssetRegistry.getAllAssets().stream().map((Function<? super List<CommonAssetRegistry.PackAsset>, ?>)List::getLast).map((Function<? super Object, ?>)CommonAssetRegistry.PackAsset::asset).map((Function<? super Object, ?>)CommonAsset::toPacket).toArray(Asset[]::new));
        CommonAssetModule.instance = this;
    }
    
    @Override
    protected void setup() {
        this.getEventRegistry().register(SendCommonAssetsEvent.class, this::onSendCommonAssets);
        this.getEventRegistry().register((short)(-32), LoadAssetEvent.class, event -> {
            for (final AssetPack pack : AssetModule.get().getAssetPacks()) {
                this.loadCommonAssets(pack, event.getBootStart());
            }
            return;
        });
        this.getEventRegistry().register((short)(-32), AssetPackRegisterEvent.class, event -> this.loadCommonAssets(event.getAssetPack(), System.nanoTime()));
        this.getEventRegistry().register(AssetPackUnregisterEvent.class, event -> this.removeCommonAssets(event.getAssetPack()));
    }
    
    private void removeCommonAssets(@Nonnull final AssetPack assetPack) {
        this.unregisterAssetMonitor(assetPack);
        final List<CommonAssetRegistry.PackAsset> removedAssets = new ObjectArrayList<CommonAssetRegistry.PackAsset>();
        final List<CommonAsset> updatedAssets = new ObjectArrayList<CommonAsset>();
        final Collection<List<CommonAssetRegistry.PackAsset>> allAssets = CommonAssetRegistry.getAllAssets();
        for (final List<CommonAssetRegistry.PackAsset> assets : allAssets) {
            for (final CommonAssetRegistry.PackAsset asset : assets) {
                if (asset.pack().equals(assetPack.getName())) {
                    final BooleanObjectPair<CommonAssetRegistry.PackAsset> removed = CommonAssetRegistry.removeCommonAssetByName(asset.pack(), asset.asset().getName());
                    if (removed != null) {
                        if (removed.firstBoolean()) {
                            updatedAssets.add(removed.second().asset());
                        }
                        else {
                            removedAssets.add(removed.second());
                        }
                    }
                    this.assets.invalidate();
                }
            }
        }
        this.sendRemoveAssets(removedAssets, false);
        this.sendAssets(updatedAssets, false);
        Universe.get().broadcastPacketNoCache(new RequestCommonAssetsRebuild());
    }
    
    public void loadCommonAssets(@Nonnull final AssetPack pack, final long bootTime) {
        final Path assetPath = pack.getRoot();
        HytaleLogger.getLogger().at(Level.INFO).log("Loading common assets from: %s", assetPath);
        final long start = System.nanoTime();
        if (this.readCommonAssetsIndexHashes(pack)) {
            final int duplicateAssetCount = CommonAssetRegistry.getDuplicateAssetCount();
            if (duplicateAssetCount > 0) {
                this.getLogger().at(Level.WARNING).log("Duplicated Asset Count: %s", duplicateAssetCount);
            }
            HytaleLogger.getLogger().at(Level.INFO).log("Loading common assets phase completed! Boot time %s, Took %s", FormatUtil.nanosToString(System.nanoTime() - bootTime), FormatUtil.nanosToString(System.nanoTime() - start));
            return;
        }
        final Path commonPath = pack.getRoot().resolve("Common");
        final AssetMonitor assetMonitor = AssetModule.get().getAssetMonitor();
        if (assetMonitor != null && !pack.isImmutable() && Files.isDirectory(commonPath, new LinkOption[0])) {
            assetMonitor.monitorDirectoryFiles(commonPath, new CommonAssetMonitorHandler(pack, commonPath));
        }
        this.readCommonAssetsIndexCache(pack);
        try {
            this.walkFileTree(pack);
        }
        catch (final IOException e) {
            throw SneakyThrow.sneakyThrow(e);
        }
        final int duplicateAssetCount2 = CommonAssetRegistry.getDuplicateAssetCount();
        if (duplicateAssetCount2 > 0) {
            this.getLogger().at(Level.WARNING).log("Duplicated Asset Count: %s", duplicateAssetCount2);
        }
        HytaleLogger.getLogger().at(Level.INFO).log("Loading common assets phase completed! Boot time %s, Took %s", FormatUtil.nanosToString(System.nanoTime() - bootTime), FormatUtil.nanosToString(System.nanoTime() - start));
        Universe.get().broadcastPacketNoCache(new RequestCommonAssetsRebuild());
    }
    
    public <T extends CommonAsset> void addCommonAsset(final String pack, @Nonnull final T asset) {
        this.addCommonAsset(pack, asset, true);
    }
    
    public <T extends CommonAsset> void addCommonAsset(final String pack, @Nonnull final T asset, final boolean log) {
        final CommonAssetRegistry.AddCommonAssetResult result = CommonAssetRegistry.addCommonAsset(pack, asset);
        final CommonAssetRegistry.PackAsset newAsset = result.getNewPackAsset();
        final CommonAssetRegistry.PackAsset oldAsset = result.getPreviousNameAsset();
        if (oldAsset != null && oldAsset.asset().getHash().equals(newAsset.asset().getHash())) {
            if (log) {
                this.getLogger().at(Level.INFO).log("Didn't change: %s", asset.getName());
            }
            return;
        }
        if (oldAsset == null) {
            if (log) {
                this.getLogger().at(Level.INFO).log("Created: %s", newAsset);
            }
        }
        else if (log) {
            this.getLogger().at(Level.INFO).log("Reloaded: %s - Old Hash: %s", newAsset, oldAsset.asset().getHash());
        }
        final String messageId = (oldAsset == null) ? "server.general.assetstore.reloadAssets" : "server.general.assetstore.reloadAssets";
        final String iconPath = (oldAsset == null) ? "Icons/AssetNotifications/IconCheckmark.png" : "Icons/AssetNotifications/AssetReloaded.png";
        final String messageColor = (oldAsset == null) ? "#06EE92" : "#A7AfA7";
        NotificationUtil.sendNotificationToUniverse(Message.translation(messageId).color(messageColor).param("class", "Common"), Message.raw(newAsset.pack() + ":" + newAsset.asset().getName()), iconPath, NotificationStyle.Success);
        if (!result.getActiveAsset().equals(newAsset)) {
            return;
        }
        this.assets.invalidate();
        BlockyAnimationCache.invalidate(newAsset.asset().getName());
        if (Universe.get().getPlayerCount() > 0) {
            this.sendAsset(newAsset.asset(), false);
        }
    }
    
    @Nullable
    public Asset[] getRequiredAssets() {
        return this.assets.get();
    }
    
    private boolean readCommonAssetsIndexHashes(@Nonnull final AssetPack pack) {
        final Path assetPath = pack.getRoot();
        final Path commonPath = assetPath.resolve("Common");
        final Path assetHashFile = assetPath.resolve("CommonAssetsIndex.hashes");
        if (Files.isRegularFile(assetHashFile, new LinkOption[0])) {
            final long loadHashesStart = System.nanoTime();
            int loadedAssetCount = 0;
            try {
                final BufferedReader reader = Files.newBufferedReader(assetHashFile);
                try {
                    int version = 0;
                    int i = 0;
                    while (true) {
                        final String line = reader.readLine();
                        if (line == null) {
                            if (reader != null) {
                                reader.close();
                                break;
                            }
                            break;
                        }
                        else {
                            if (line.startsWith("VERSION=")) {
                                version = Integer.parseInt(line.substring("VERSION=".length()));
                                this.getLogger().at(Level.FINEST).log("Version set to %d from CommonAssetsIndex.hashes:L%d '%s'", version, i, line);
                                if (version > 0) {
                                    throw new IllegalArgumentException(String.format("Unsupported version %d in CommonAssetsIndex.hashes %d > %d", version, version, 0));
                                }
                            }
                            else {
                                final String[] split = line.split(" ", 2);
                                if (split.length != 2) {
                                    this.getLogger().at(Level.WARNING).log("Corrupt line in CommonAssetsIndex.hashes:L%d '%s'", i, line);
                                }
                                else {
                                    final String hash = split[0];
                                    if (hash.length() != 64 && !CommonAsset.HASH_PATTERN.matcher(hash).matches()) {
                                        this.getLogger().at(Level.WARNING).log("Corrupt line in CommonAssetsIndex.hashes:L%d '%s'", i, line);
                                    }
                                    else {
                                        final String name = split[1];
                                        this.addCommonAsset(pack.getName(), new FileCommonAsset(commonPath.resolve(name), name, hash, null), false);
                                        this.getLogger().at(Level.FINEST).log("Loaded asset info from CommonAssetsIndex.hashes:L%d '%s'", i, name);
                                        ++loadedAssetCount;
                                    }
                                }
                            }
                            ++i;
                        }
                    }
                }
                catch (final Throwable t) {
                    if (reader != null) {
                        try {
                            reader.close();
                        }
                        catch (final Throwable exception) {
                            t.addSuppressed(exception);
                        }
                    }
                    throw t;
                }
            }
            catch (final IOException e) {
                this.getLogger().at(Level.WARNING).withCause(e).log("Failed to load hashes from CommonAssetsIndex.hashes");
                return false;
            }
            final long loadHashesEnd = System.nanoTime();
            final long loadHashesDiff = loadHashesEnd - loadHashesStart;
            this.getLogger().at(Level.INFO).log("Took %s to load %d assets from CommonAssetsIndex.hashes file.", FormatUtil.nanosToString(loadHashesDiff), loadedAssetCount);
            return true;
        }
        return false;
    }
    
    private void readCommonAssetsIndexCache(@Nonnull final AssetPack pack) {
        final Path assetPath = pack.getRoot();
        final Path commonPath = assetPath.resolve("Common");
        final Path assetCacheFile = assetPath.resolve("CommonAssetsIndex.cache");
        if (!Files.isRegularFile(assetCacheFile, new LinkOption[0])) {
            return;
        }
        final long loadCacheStart = System.nanoTime();
        final AtomicInteger loadedAssetCount = new AtomicInteger();
        final List<CompletableFuture<Void>> futures = new ObjectArrayList<CompletableFuture<Void>>();
        try {
            final BufferedReader reader = Files.newBufferedReader(assetCacheFile);
            try {
                int version = 0;
                int i = 0;
                while (true) {
                    final String line = reader.readLine();
                    if (line == null) {
                        if (reader != null) {
                            reader.close();
                            break;
                        }
                        break;
                    }
                    else {
                        if (line.startsWith("VERSION=")) {
                            version = Integer.parseInt(line.substring("VERSION=".length()));
                            this.getLogger().at(Level.FINEST).log("Version set to %d from CommonAssetsIndex.cache:L%d '%s'", version, i, line);
                            if (version > 1) {
                                throw new IllegalArgumentException(String.format("Unsupported version %d in CommonAssetsIndex.cache %d > %d", version, version, 1));
                            }
                        }
                        else {
                            final int indexOne = line.indexOf(32);
                            final int indexTwo = line.indexOf(32, indexOne + 1);
                            if (indexTwo < 0) {
                                this.getLogger().at(Level.WARNING).log("Corrupt line in CommonAssetsIndex.cache:L%d '%s'", i, line);
                            }
                            else {
                                final String hash = line.substring(0, indexOne);
                                if (hash.length() != 64 && !CommonAsset.HASH_PATTERN.matcher(hash).matches()) {
                                    this.getLogger().at(Level.WARNING).log("Corrupt line in CommonAssetsIndex.cache:L%d '%s'", i, line);
                                }
                                else {
                                    final long timestampLong = Long.parseLong(line, indexOne + 1, indexTwo, 10);
                                    Instant timestamp;
                                    if (version > 0) {
                                        timestamp = Instant.ofEpochSecond(timestampLong);
                                    }
                                    else {
                                        final long timestampMillis = timestampLong / 10000L;
                                        timestamp = CommonAssetModule.TICK_TIMESTAMP_ORIGIN.plusMillis(timestampMillis);
                                    }
                                    final String name = line.substring(indexTwo + 1);
                                    final Path file = commonPath.resolve(name);
                                    final int lineNumber = i;
                                    futures.add(CompletableFuture.supplyAsync(() -> {
                                        BasicFileAttributes attributes;
                                        try {
                                            attributes = Files.readAttributes(file, BasicFileAttributes.class, new LinkOption[0]);
                                        }
                                        catch (final IOException ignored) {
                                            return null;
                                        }
                                        if (!attributes.isRegularFile()) {
                                            return null;
                                        }
                                        else {
                                            final Instant lastModified = attributes.lastModifiedTime().toInstant().truncatedTo(ChronoUnit.SECONDS);
                                            if (timestamp.equals(lastModified)) {
                                                this.addCommonAsset(pack.getName(), new FileCommonAsset(file, name, hash, null), false);
                                                this.getLogger().at(Level.FINEST).log("Loaded asset info from CommonAssetsIndex.cache:L%d '%s'", lineNumber, name);
                                                loadedAssetCount.getAndIncrement();
                                            }
                                            else {
                                                this.getLogger().at(Level.FINEST).log("Skipped outdated asset from CommonAssetsIndex.cache:L%d '%s', Timestamp: %s, Last Modified: %s", lineNumber, name, timestamp, lastModified);
                                            }
                                            return null;
                                        }
                                    }));
                                }
                            }
                        }
                        ++i;
                    }
                }
            }
            catch (final Throwable t) {
                if (reader != null) {
                    try {
                        reader.close();
                    }
                    catch (final Throwable exception) {
                        t.addSuppressed(exception);
                    }
                }
                throw t;
            }
        }
        catch (final IOException e) {
            this.getLogger().at(Level.WARNING).withCause(e).log("Failed to load hashes from CommonAssetsIndex.cache");
        }
        CompletableFuture.allOf((CompletableFuture<?>[])futures.toArray(CompletableFuture[]::new)).join();
        final long loadCacheEnd = System.nanoTime();
        final long loadCacheDiff = loadCacheEnd - loadCacheStart;
        this.getLogger().at(Level.INFO).log("Took %s to load %d assets from CommonAssetsIndex.cache file.", FormatUtil.nanosToString(loadCacheDiff), loadedAssetCount.get());
    }
    
    private void walkFileTree(@Nonnull final AssetPack pack) throws IOException {
        final Path assetPath = pack.getRoot();
        final Path commonPath = assetPath.resolve("Common").toAbsolutePath();
        if (!Files.exists(commonPath, new LinkOption[0])) {
            return;
        }
        final int commonPathSubStringIndex = commonPath.toString().length() + 1;
        final long walkFileTreeStart = System.nanoTime();
        final ObjectArrayList<CompletableFuture<Void>> futures = new ObjectArrayList<CompletableFuture<Void>>();
        Files.walkFileTree(commonPath, FileUtil.DEFAULT_WALK_TREE_OPTIONS_SET, Integer.MAX_VALUE, new SimpleFileVisitor<Path>() {
            @Nonnull
            @Override
            public FileVisitResult visitFile(@Nonnull final Path path, @Nonnull final BasicFileAttributes attrs) throws IOException {
                if (!attrs.isRegularFile()) {
                    return FileVisitResult.CONTINUE;
                }
                final Path fileName = path.getFileName();
                if (CommonAssetModule.IGNORED_FILES.contains(fileName)) {
                    final String name = PatternUtil.replaceBackslashWithForwardSlash(path.toString().substring(commonPathSubStringIndex));
                    CommonAssetModule.this.getLogger().at(Level.FINEST).log("Skipping ignored file at %s", name);
                    return FileVisitResult.CONTINUE;
                }
                if (fileName.toString().endsWith(".hash")) {
                    Files.deleteIfExists(path);
                    return FileVisitResult.CONTINUE;
                }
                final String name = PatternUtil.replaceBackslashWithForwardSlash(path.toString().substring(commonPathSubStringIndex));
                if (CommonAssetRegistry.hasCommonAsset(pack, name)) {
                    return FileVisitResult.CONTINUE;
                }
                CommonAssetModule.this.getLogger().at(Level.FINER).log("Loading asset: %s", name);
                futures.add(CompletableFuture.supplyAsync((Supplier<Object>)SneakyThrow.sneakySupplier(() -> Files.readAllBytes(path))).thenAcceptAsync(bytes -> {
                    final Object val$pack = pack;
                    final FileCommonAsset asset = new FileCommonAsset(path, name, bytes);
                    CommonAssetModule.this.addCommonAsset(pack.getName(), asset, false);
                    CommonAssetModule.this.getLogger().at(Level.FINER).log("Loaded asset: %s", asset);
                    return;
                }).exceptionally(throwable -> {
                    CommonAssetModule.this.getLogger().at(Level.FINE).withCause(throwable).log("Failed to load asset: %s", name);
                    throw SneakyThrow.sneakyThrow(throwable);
                }));
                return FileVisitResult.CONTINUE;
            }
        });
        CompletableFuture.allOf((CompletableFuture<?>[])futures.toArray(CompletableFuture[]::new)).join();
        this.assets.invalidate();
        final long walkFileTreeEnd = System.nanoTime();
        final long walkFileTreeDiff = walkFileTreeEnd - walkFileTreeStart;
        this.getLogger().at(Level.INFO).log("Took %s to walk file tree and load %d assets.", FormatUtil.nanosToString(walkFileTreeDiff), futures.size());
    }
    
    private void unregisterAssetMonitor(@Nonnull final AssetPack pack) {
        final AssetMonitor assetMonitor = AssetModule.get().getAssetMonitor();
        if (assetMonitor != null) {
            assetMonitor.removeMonitorDirectoryFiles(pack.getRoot().resolve("Common"), pack);
        }
    }
    
    private void reloadAsset(@Nonnull final List<CompletableFuture<Void>> addedOrUpdatedAssets, final String pack, @Nonnull final Path file, @Nonnull final String name) {
        this.getLogger().at(Level.FINEST).log("Reloading: %s", file);
        addedOrUpdatedAssets.add(CompletableFuture.supplyAsync((Supplier<Object>)SneakyThrow.sneakySupplier(() -> Files.readAllBytes(file))).thenAcceptAsync(bytes -> this.addCommonAsset(pack, new FileCommonAsset(file, name, bytes))).exceptionally(throwable -> {
            if (throwable instanceof NoSuchFileException) {
                throwable = new SkipSentryException(throwable);
            }
            this.getLogger().at(Level.SEVERE).withCause(throwable).log("Failed to reload asset: %s", file);
            return null;
        }));
    }
    
    private void onSendCommonAssets(@Nonnull final SendCommonAssetsEvent event) {
        this.sendAssetsToPlayer(event.getPacketHandler(), event.getRequestedAssets(), true);
    }
    
    public void sendAssetsToPlayer(@Nonnull final PacketHandler packetHandler, @Nullable final Asset[] requested, final boolean forceRebuild) {
        final List<CommonAsset> toSend = new ObjectArrayList<CommonAsset>();
        if (requested != null) {
            for (final Asset toSendAsset : requested) {
                final CommonAsset asset = CommonAssetRegistry.getByHash(toSendAsset.hash);
                Objects.requireNonNull(asset, toSendAsset.hash);
                toSend.add(asset);
            }
        }
        else {
            for (final List<CommonAssetRegistry.PackAsset> asset2 : CommonAssetRegistry.getAllAssets()) {
                toSend.add(asset2.getLast().asset());
            }
        }
        this.getLogger().at(Level.FINE).log("%s requested %d assets!", packetHandler.getIdentifier(), toSend.size());
        this.sendAssetsToPlayer(packetHandler, toSend, forceRebuild);
    }
    
    public void sendAssets(@Nonnull final List<CommonAsset> toSend, final boolean forceRebuild) {
        for (int i = 0; i < toSend.size(); ++i) {
            final CommonAsset thisAsset = toSend.get(i);
            final byte[] allBytes = thisAsset.getBlob().join();
            final byte[][] parts = ArrayUtil.split(allBytes, 2621440);
            final Packet[] packets = new Packet[2 + parts.length];
            packets[0] = new AssetInitialize(thisAsset.toPacket(), allBytes.length);
            for (int partIndex = 0; partIndex < parts.length; ++partIndex) {
                packets[1 + partIndex] = new AssetPart(parts[partIndex]);
            }
            packets[packets.length - 1] = new AssetFinalize();
            Universe.get().broadcastPacket(packets);
        }
        if (!toSend.isEmpty() && forceRebuild) {
            Universe.get().broadcastPacketNoCache(new RequestCommonAssetsRebuild());
        }
    }
    
    public void sendAssetsToPlayer(@Nonnull final PacketHandler packetHandler, @Nonnull final List<CommonAsset> toSend, final boolean forceRebuild) {
        for (int i = 0; i < toSend.size(); ++i) {
            final int thisPercent = MathUtil.getPercentageOf(i, toSend.size());
            final CommonAsset thisAsset = toSend.get(i);
            final byte[] allBytes = thisAsset.getBlob().join();
            final byte[][] parts = ArrayUtil.split(allBytes, 2621440);
            final Packet[] packets = new Packet[2 + parts.length * 2];
            packets[0] = new AssetInitialize(thisAsset.toPacket(), allBytes.length);
            for (int partIndex = 0; partIndex < parts.length; ++partIndex) {
                packets[1 + partIndex * 2] = new WorldLoadProgress("Loading asset " + thisAsset.getName(), thisPercent, 100 * partIndex / parts.length);
                packets[1 + partIndex * 2 + 1] = new AssetPart(parts[partIndex]);
            }
            packets[packets.length - 1] = new AssetFinalize();
            packetHandler.write(packets);
        }
        if (!toSend.isEmpty() && forceRebuild) {
            packetHandler.writeNoCache(new RequestCommonAssetsRebuild());
        }
    }
    
    public void sendAsset(@Nonnull final CommonAsset asset, final boolean forceRebuild) {
        asset.getBlob().whenComplete((allBytes, throwable) -> {
            if (throwable != null) {
                this.getLogger().at(Level.WARNING).log("Failed to send asset: %s, %s", asset.getName(), asset.getHash());
            }
            else {
                final byte[][] parts = ArrayUtil.split(allBytes, 2621440);
                final Packet[] packets = new Packet[2 + (forceRebuild ? 1 : 0) + parts.length];
                packets[0] = new AssetInitialize(asset.toPacket(), allBytes.length);
                for (int i = 0; i < parts.length; ++i) {
                    packets[1 + i] = new AssetPart(parts[i]);
                }
                packets[1 + parts.length] = new AssetFinalize();
                if (forceRebuild) {
                    packets[2 + parts.length] = new RequestCommonAssetsRebuild();
                }
                Universe.get().broadcastPacket(packets);
            }
        });
    }
    
    public void sendRemoveAssets(@Nonnull final List<CommonAssetRegistry.PackAsset> assets, final boolean forceRebuild) {
        final int size = assets.size();
        final Asset[] asset_ = new Asset[size];
        final String messageRemovalKey = "server.general.assetstore.removedAssets";
        final String color = "#FF3874";
        final String icon = "Icons/AssetNotifications/Trash.png";
        final Message message = Message.translation("server.general.assetstore.removedAssets").param("class", "Common").color("#FF3874");
        final int packetCountThreshold = 5;
        final int packetsCount = 1 + (forceRebuild ? 1 : 0) + ((assets.size() < 5) ? assets.size() : 1);
        final Packet[] packets = new Packet[packetsCount];
        int i = 0;
        for (final CommonAssetRegistry.PackAsset asset : assets) {
            asset_[i++] = asset.asset().toPacket();
        }
        if (assets.size() < 5) {
            i = 0;
            for (CommonAssetRegistry.PackAsset asset : assets) {
                final Message assetName = Message.raw(asset.pack() + ":" + asset.asset().getName()).color("#FF3874");
                packets[i++] = new Notification(message.getFormattedMessage(), assetName.getFormattedMessage(), "Icons/AssetNotifications/Trash.png", null, NotificationStyle.Default);
            }
            packets[i++] = new RemoveAssets(asset_);
            if (forceRebuild) {
                packets[i++] = new RequestCommonAssetsRebuild();
            }
        }
        else {
            final Message secondaryMessage = Message.translation("server.general.assetstore.removedAssetsSecondaryGeneric").param("count", assets.size());
            packets[0] = new Notification(message.getFormattedMessage(), secondaryMessage.getFormattedMessage(), "Icons/AssetNotifications/Trash.png", null, NotificationStyle.Default);
            packets[1] = new RemoveAssets(asset_);
            if (forceRebuild) {
                packets[2] = new RequestCommonAssetsRebuild();
            }
        }
        Universe.get().broadcastPacket(packets);
    }
    
    static {
        MANIFEST = PluginManifest.corePlugin(CommonAssetModule.class).depends(AssetModule.class).build();
        IGNORED_FILES = Set.of(Path.of(".DS_Store", new String[0]), Path.of("Thumbs.db", new String[0]));
        TICK_TIMESTAMP_ORIGIN = Instant.parse("0001-01-01T00:00:00Z");
    }
    
    private class CommonAssetMonitorHandler implements AssetMonitorHandler
    {
        private final AssetPack pack;
        private final Path commonPath;
        
        public CommonAssetMonitorHandler(final AssetPack pack, final Path commonPath) {
            this.pack = pack;
            this.commonPath = commonPath;
        }
        
        @Override
        public Object getKey() {
            return this.pack;
        }
        
        @Override
        public boolean test(final Path path, final EventKind eventKind) {
            return !CommonAssetModule.IGNORED_FILES.contains(path.getFileName());
        }
        
        @Override
        public void accept(final Map<Path, EventKind> map) {
            final List<Path> createdOrModifiedFilesToLoad = new ObjectArrayList<Path>();
            final List<Path> removedFilesToUnload = new ObjectArrayList<Path>();
            final List<Path> createdOrModifiedDirectories = new ObjectArrayList<Path>();
            final List<Path> removedFilesAndDirectories = new ObjectArrayList<Path>();
            for (final Map.Entry<Path, EventKind> entry : map.entrySet()) {
                final Path path = entry.getKey();
                final EventKind eventKind = entry.getValue();
                switch (eventKind) {
                    case ENTRY_CREATE: {
                        if (Files.isDirectory(path, new LinkOption[0])) {
                            CommonAssetModule.this.getLogger().at(Level.INFO).log("Directory Created: %s", path);
                            try (final Stream<Path> stream = Files.walk(path, FileUtil.DEFAULT_WALK_TREE_OPTIONS_ARRAY)) {
                                stream.forEach(child -> {
                                    BasicFileAttributes attributes;
                                    try {
                                        attributes = Files.readAttributes(child, BasicFileAttributes.class, new LinkOption[0]);
                                    }
                                    catch (final IOException ignored) {
                                        return;
                                    }
                                    if (attributes.isDirectory()) {
                                        createdOrModifiedDirectories.add(path);
                                    }
                                    else if (attributes.isRegularFile()) {
                                        createdOrModifiedFilesToLoad.add(child);
                                    }
                                    return;
                                });
                                if (stream == null) {
                                    continue;
                                }
                            }
                            catch (final IOException e) {
                                CommonAssetModule.this.getLogger().at(Level.SEVERE).withCause(e).log("Failed to reload assets in directory: %s", path);
                            }
                            continue;
                        }
                        CommonAssetModule.this.getLogger().at(Level.INFO).log("File Created: %s", path);
                        createdOrModifiedFilesToLoad.add(path);
                        continue;
                    }
                    case ENTRY_DELETE: {
                        CommonAssetModule.this.getLogger().at(Level.INFO).log("Deleted: %s", path);
                        removedFilesAndDirectories.add(path);
                        final Path relative = PathUtil.relativize(this.commonPath, path);
                        final String name = PatternUtil.replaceBackslashWithForwardSlash(relative.toString());
                        final List<CommonAsset> commonAssets = CommonAssetRegistry.getCommonAssetsStartingWith(this.pack.getName(), name);
                        for (final CommonAsset asset : commonAssets) {
                            removedFilesToUnload.add(this.commonPath.resolve(asset.getName()));
                        }
                        continue;
                    }
                    case ENTRY_MODIFY: {
                        if (Files.isDirectory(path, new LinkOption[0])) {
                            CommonAssetModule.this.getLogger().at(Level.INFO).log("Directory Modified: %s", path);
                            createdOrModifiedDirectories.add(path);
                            continue;
                        }
                        CommonAssetModule.this.getLogger().at(Level.INFO).log("File Modified: %s", path);
                        createdOrModifiedFilesToLoad.add(path);
                        continue;
                    }
                    default: {
                        throw new IllegalArgumentException("Unknown eventKind " + String.valueOf(eventKind));
                    }
                }
            }
            if (!removedFilesAndDirectories.isEmpty() || !createdOrModifiedFilesToLoad.isEmpty() || !createdOrModifiedDirectories.isEmpty()) {
                final IEventDispatcher<CommonAssetMonitorEvent, CommonAssetMonitorEvent> dispatchFor = HytaleServer.get().getEventBus().dispatchFor((Class<? super CommonAssetMonitorEvent>)CommonAssetMonitorEvent.class);
                if (dispatchFor.hasListener()) {
                    dispatchFor.dispatch(new CommonAssetMonitorEvent(this.pack.getName(), createdOrModifiedFilesToLoad, removedFilesToUnload, createdOrModifiedDirectories, removedFilesAndDirectories));
                }
            }
            final List<CompletableFuture<Void>> addedOrUpdatedAssets = new ObjectArrayList<CompletableFuture<Void>>();
            final List<CommonAssetRegistry.PackAsset> removedAssets = new ObjectArrayList<CommonAssetRegistry.PackAsset>();
            final List<CommonAsset> updatedAssets = new ObjectArrayList<CommonAsset>();
            if (!removedFilesToUnload.isEmpty()) {
                CommonAssetModule.this.getLogger().at(Level.INFO).log("Removing deleted assets: %s", removedFilesToUnload);
                for (final Path path2 : removedFilesToUnload) {
                    final Path relativePath = PathUtil.relativize(this.commonPath, path2);
                    final String name2 = PatternUtil.replaceBackslashWithForwardSlash(relativePath.toString());
                    final BooleanObjectPair<CommonAssetRegistry.PackAsset> removed = CommonAssetRegistry.removeCommonAssetByName(this.pack.getName(), name2);
                    if (removed != null) {
                        if (removed.firstBoolean()) {
                            updatedAssets.add(removed.second().asset());
                        }
                        else {
                            removedAssets.add(removed.second());
                        }
                    }
                    CommonAssetModule.this.assets.invalidate();
                }
                CommonAssetModule.this.sendRemoveAssets(removedAssets, false);
                CommonAssetModule.this.sendAssets(updatedAssets, false);
            }
            if (!createdOrModifiedFilesToLoad.isEmpty()) {
                CommonAssetModule.this.getLogger().at(Level.INFO).log("Reloading assets: %s", createdOrModifiedFilesToLoad);
                for (final Path path2 : createdOrModifiedFilesToLoad) {
                    final Path relative2 = PathUtil.relativize(this.commonPath, path2);
                    final String name2 = PatternUtil.replaceBackslashWithForwardSlash(relative2.toString());
                    CommonAssetModule.this.reloadAsset(addedOrUpdatedAssets, this.pack.getName(), path2, name2);
                }
                CompletableFuture.allOf((CompletableFuture<?>[])addedOrUpdatedAssets.toArray(CompletableFuture[]::new)).thenAccept(v -> Universe.get().broadcastPacketNoCache(new RequestCommonAssetsRebuild()));
            }
            else {
                Universe.get().broadcastPacketNoCache(new RequestCommonAssetsRebuild());
            }
        }
    }
}
