package speiger.src.coreengine.assets.impl;

import java.io.IOException;
import java.nio.file.FileSystems;
import java.nio.file.FileVisitResult;
import java.nio.file.Files;
import java.nio.file.LinkOption;
import java.nio.file.Path;
import java.nio.file.SimpleFileVisitor;
import java.nio.file.StandardWatchEventKinds;
import java.nio.file.WatchEvent;
import java.nio.file.WatchEvent.Kind;
import java.nio.file.WatchKey;
import java.nio.file.WatchService;
import java.nio.file.attribute.BasicFileAttributes;
import java.util.Map;
import java.util.function.Predicate;

import speiger.src.collections.objects.maps.impl.hash.Object2ObjectOpenHashMap;

public class FolderWatcher<T> {
    WatchService watcher;
    Map<WatchKey, PathFlags<T>> keys = new Object2ObjectOpenHashMap<>();
    
    public FolderWatcher() throws IOException {
		watcher = FileSystems.getDefault().newWatchService();
    }
    
    public FolderWatcher(WatchService service) {
    	this.watcher = service;
    }
    
    public void reset() {
    	keys.keySet().forEach(WatchKey::cancel);
    	keys.clear();
    }
    
    public void close() throws IOException {
    	watcher.close();
    	keys.clear();
    }
    
    public void register(Path basePath, T owner, boolean recursive) throws IOException {
    	if(recursive) {
    		Files.walkFileTree(basePath, new SimpleFileVisitor<Path>() {
    			@Override
    			public FileVisitResult preVisitDirectory(Path dir, BasicFileAttributes attrs) throws IOException {
    				keys.put(register(dir), new PathFlags<>(dir, owner, true));
    				return FileVisitResult.CONTINUE;
    			}
    		});
    		return;
    	}
    	keys.put(register(basePath), new PathFlags<>(basePath, owner, recursive));
    }
    
    private WatchKey register(Path path) throws IOException {
    	return path.register(watcher, StandardWatchEventKinds.ENTRY_CREATE, StandardWatchEventKinds.ENTRY_MODIFY, StandardWatchEventKinds.ENTRY_DELETE);
    }
    
    public void pollEvents(FolderListener<T> output) {
    	WatchKey key = null;
    	boolean checkInvalidation = false;
    	while((key = watcher.poll()) != null) {
    		PathFlags<T> flags = keys.get(key);
    		if(flags == null) continue;
    		for(WatchEvent<?> event : key.pollEvents()) {
    			if(event.kind() == StandardWatchEventKinds.OVERFLOW) continue;
    			Path child = flags.resolve((Path)event.context());
    			ChangeType type = toType(event.kind());
    			if(type == ChangeType.ADD && flags.recursion() && Files.isDirectory(child, LinkOption.NOFOLLOW_LINKS)) {
    				try { register(child, flags.owner(), true); }
					catch(Exception e) { e.printStackTrace(); }
    			}
    			else if(type == ChangeType.REMOVE && !checkInvalidation) {
    				checkInvalidation = true;
    			}
    			output.onChanged(child, type, flags.owner());
    		}
    		if(!key.reset()) {
    			keys.remove(key);
    		}
    	}
    	if(checkInvalidation) {
    		keys.keySet().removeIf(((Predicate<WatchKey>)WatchKey::isValid).negate());
    	}
    }
    
    private ChangeType toType(Kind<?> kind) {
    	if(kind == StandardWatchEventKinds.ENTRY_CREATE) return ChangeType.ADD;
    	else if(kind == StandardWatchEventKinds.ENTRY_MODIFY) return ChangeType.MODIFIED;
    	return ChangeType.REMOVE;
    }
    
    public interface FolderListener<T> {
    	public void onChanged(Path path, ChangeType type, T owner);
    }
    
    private record PathFlags<T>(Path folder, T owner, boolean recursion) {
    	public Path resolve(Path subFolder) {
    		return folder.resolve(subFolder);
    	}
    }
    
	public static enum ChangeType {
		ADD,
		MODIFIED,
		REMOVE;
	}
}
