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

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

import java.util.concurrent.Executors;
import com.hypixel.hytale.server.core.util.concurrent.ThreadUtil;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.ScheduledFuture;
import java.util.Iterator;
import java.nio.file.AccessDeniedException;
import java.io.FileNotFoundException;
import java.nio.file.NoSuchFileException;
import com.hypixel.hytale.logger.sentry.SkipSentryException;
import java.util.function.Function;
import com.hypixel.hytale.sneakythrow.SneakyThrow;
import it.unimi.dsi.fastutil.objects.ObjectArrayList;
import java.util.logging.Level;
import java.nio.file.Files;
import java.nio.file.LinkOption;
import java.io.IOException;
import java.util.concurrent.ConcurrentHashMap;
import javax.annotation.Nonnull;
import java.util.List;
import java.nio.file.Path;
import java.util.Map;
import java.util.concurrent.ScheduledExecutorService;
import com.hypixel.hytale.logger.HytaleLogger;

public class AssetMonitor
{
    public static final HytaleLogger LOGGER;
    private static final ScheduledExecutorService EXECUTOR;
    private final Map<Path, List<AssetMonitorHandler>> directoryMonitors;
    private final Map<Path, FileChangeTask> fileChangeTasks;
    private final Map<Path, Map<AssetMonitorHandler, DirectoryHandlerChangeTask>> directoryHandlerChangeTasks;
    @Nonnull
    private final PathWatcherThread pathWatcherThread;
    
    public AssetMonitor() throws IOException {
        this.directoryMonitors = new ConcurrentHashMap<Path, List<AssetMonitorHandler>>();
        this.fileChangeTasks = new ConcurrentHashMap<Path, FileChangeTask>();
        this.directoryHandlerChangeTasks = new ConcurrentHashMap<Path, Map<AssetMonitorHandler, DirectoryHandlerChangeTask>>();
        (this.pathWatcherThread = new PathWatcherThread(this::onChange)).start();
    }
    
    public void shutdown() {
        this.pathWatcherThread.shutdown();
    }
    
    public void monitorDirectoryFiles(@Nonnull final Path path, @Nonnull final AssetMonitorHandler handler) {
        if (!Files.isDirectory(path, new LinkOption[0])) {
            throw new IllegalArgumentException(String.valueOf(path));
        }
        try {
            final Path normalize = path.toAbsolutePath().normalize();
            AssetMonitor.LOGGER.at(Level.FINE).log("Monitoring Directory: %s", normalize);
            this.directoryMonitors.computeIfAbsent(normalize, SneakyThrow.sneakyFunction(k -> {
                this.pathWatcherThread.addPath(k);
                return new ObjectArrayList();
            })).add(handler);
        }
        catch (final Exception e) {
            AssetMonitor.LOGGER.at(Level.SEVERE).withCause(new SkipSentryException(e)).log("Failed to monitor directory: %s", path);
        }
    }
    
    public void removeMonitorDirectoryFiles(@Nonnull final Path path, @Nonnull final Object key) {
        if (!Files.isDirectory(path, new LinkOption[0])) {
            throw new IllegalArgumentException(String.valueOf(path));
        }
        try {
            final Path normalize = path.toAbsolutePath().normalize();
            AssetMonitor.LOGGER.at(Level.FINE).log("Monitoring Directory: %s", normalize);
            this.directoryMonitors.computeIfAbsent(normalize, SneakyThrow.sneakyFunction(k -> {
                this.pathWatcherThread.addPath(k);
                return new ObjectArrayList();
            })).removeIf(v -> v.getKey().equals(key));
        }
        catch (final Exception e) {
            AssetMonitor.LOGGER.at(Level.SEVERE).withCause(new SkipSentryException(e)).log("Failed to monitor directory: %s", path);
        }
    }
    
    protected void onChange(@Nonnull final Path file, final EventKind eventKind) {
        AssetMonitor.LOGGER.at(Level.FINER).log("onChange: %s of %s", file, eventKind);
        final Path path = file.toAbsolutePath().normalize();
        final FileChangeTask oldTask = this.fileChangeTasks.remove(path);
        if (oldTask != null) {
            oldTask.cancelSchedule();
        }
        for (final Map<AssetMonitorHandler, DirectoryHandlerChangeTask> tasks : this.directoryHandlerChangeTasks.values()) {
            for (final DirectoryHandlerChangeTask task : tasks.values()) {
                task.removePath(path);
            }
        }
        final boolean createdOrModified = eventKind == EventKind.ENTRY_CREATE || eventKind == EventKind.ENTRY_MODIFY;
        if (createdOrModified && !Files.exists(path, new LinkOption[0])) {
            AssetMonitor.LOGGER.at(Level.WARNING).log("The asset file '%s' was deleted before we could load/update it!", path);
            return;
        }
        try {
            this.fileChangeTasks.put(path, new FileChangeTask(this, path, new PathEvent(eventKind, System.nanoTime())));
        }
        catch (final NoSuchFileException | FileNotFoundException | AccessDeniedException e) {
            AssetMonitor.LOGGER.at(Level.WARNING).log("The asset file '%s' was deleted before we could load/update it!", path);
        }
        catch (final IOException e) {
            AssetMonitor.LOGGER.at(Level.SEVERE).withCause(e).log("Failed to queue asset to be reloaded %s", path);
        }
    }
    
    public void onDelayedChange(@Nonnull final Path path, @Nonnull final PathEvent pathEvent) {
        AssetMonitor.LOGGER.at(Level.FINER).log("onDelayedChange: %s of %s", path, pathEvent);
        for (final Map.Entry<Path, List<AssetMonitorHandler>> entry : this.directoryMonitors.entrySet()) {
            final Path parent = entry.getKey();
            if (!path.startsWith(parent)) {
                continue;
            }
            final Map<AssetMonitorHandler, DirectoryHandlerChangeTask> tasks = this.directoryHandlerChangeTasks.computeIfAbsent(parent, k -> new ConcurrentHashMap());
            for (final AssetMonitorHandler directoryHandler : entry.getValue()) {
                try {
                    if (!directoryHandler.test(path, pathEvent.getEventKind())) {
                        continue;
                    }
                    tasks.computeIfAbsent(directoryHandler, handler -> new DirectoryHandlerChangeTask(this, parent, handler)).addPath(path, pathEvent);
                }
                catch (final Exception e) {
                    AssetMonitor.LOGGER.at(Level.SEVERE).withCause(e).log("Failed to run directoryHandler.test for parent: %s, %s of %s", parent, path, pathEvent);
                }
            }
        }
    }
    
    public void removeFileChangeTask(@Nonnull final FileChangeTask fileChangeTask) {
        this.fileChangeTasks.remove(fileChangeTask.getPath());
    }
    
    public void markChanged(@Nonnull final Path path) {
        for (final Map.Entry<Path, Map<AssetMonitorHandler, DirectoryHandlerChangeTask>> entry : this.directoryHandlerChangeTasks.entrySet()) {
            final Path parent = entry.getKey();
            if (!path.startsWith(parent)) {
                continue;
            }
            for (final DirectoryHandlerChangeTask hookChangeTask : entry.getValue().values()) {
                hookChangeTask.markChanged();
            }
        }
    }
    
    public void removeHookChangeTask(@Nonnull final DirectoryHandlerChangeTask directoryHandlerChangeTask) {
        final AssetMonitorHandler hook = directoryHandlerChangeTask.getHandler();
        this.directoryHandlerChangeTasks.compute(directoryHandlerChangeTask.getParent(), (k, map) -> {
            if (map == null) {
                return null;
            }
            else {
                map.remove(hook);
                return (Map<AssetMonitorHandler, DirectoryHandlerChangeTask>)(Map<AssetMonitorHandler, DirectoryHandlerChangeTask>)(map.isEmpty() ? null : map);
            }
        });
    }
    
    @Nonnull
    public static ScheduledFuture<?> runTask(@Nonnull final Runnable task, final long millisDelay) {
        return AssetMonitor.EXECUTOR.scheduleWithFixedDelay(task, millisDelay, millisDelay, TimeUnit.MILLISECONDS);
    }
    
    static {
        LOGGER = HytaleLogger.forEnclosingClass();
        EXECUTOR = Executors.newSingleThreadScheduledExecutor(ThreadUtil.daemon("AssetMonitor Thread"));
    }
}
