// Path: src/main/java/com/superzanti/serversync/files/FileManifest.java
// public class FileManifest implements Serializable {
//     public List<DirectoryEntry> directories = new ArrayList<>();
//     /**
//      * The files that the server wants to manage.
//      */
//     public List<FileEntry> files = new ArrayList<>();
// }
// 
// Path: src/main/java/com/superzanti/serversync/util/Logger.java
// public class Logger {
//     public static LoggerInstance instance = null;
//     private static final Object mutex = new Object();
//     private static Handler uiHandler;
// 
//     // Probably a heinous implementation of debounce but whatever
//     private static final long dbTimeMS = 2000L;
//     private static final ScheduledExecutorService dbRunner = Executors.newSingleThreadScheduledExecutor();
//     private static ScheduledFuture<?> waitingFlush;
// 
//     public static String getContext() {
//         return ServerSync.MODE == null ? "undefined" : ServerSync.MODE.toString();
//     }
// 
//     public static LoggerInstance getInstance() {
//         LoggerInstance result = instance;
//         if (result == null) {
//             //synchronized block to remove overhead
//             synchronized (mutex) {
//                 result = instance;
//                 if (result == null) {
//                     // if instance is null, initialize
//                     instance = result = new LoggerInstance(getContext());
//                 }
//             }
//         }
//         return result;
//     }
// 
//     public static void instantiate(String context) {
//         instance = new LoggerInstance(context);
//     }
// 
//     public static synchronized void setSystemOutput(boolean output) {
//         // enable/disable System.out logging
//         getInstance().javaLogger.setUseParentHandlers(output);
//     }
// 
//     public static synchronized void log(String s) {
//         getInstance().log(s);
//     }
// 
//     public static synchronized void error(String s) {
//         getInstance().error(s);
//     }
// 
//     public static synchronized void debug(String s) {
//         getInstance().debug(s);
//     }
// 
//     public static synchronized void debug(Exception e) {
//         getInstance().debug(Arrays.toString(e.getStackTrace()));
//     }
// 
//     public static synchronized void outputError(Object object) {
//         getInstance().debug("Failed to write object (" + object + ") to output stream");
//     }
// 
//     public static synchronized void attachUIHandler(Handler handler) {
//         if (uiHandler != null) {
//             uiHandler.close();
//             getInstance().javaLogger.removeHandler(uiHandler);
//         }
//         uiHandler = handler;
//         getInstance().javaLogger.addHandler(uiHandler);
//     }
// 
//     public static synchronized void flush() {
//         if (uiHandler != null) {
//             if (waitingFlush == null || waitingFlush.isDone()) {
//                 waitingFlush = dbRunner.schedule(() -> {
//                     uiHandler.flush();
//                 }, dbTimeMS, TimeUnit.MILLISECONDS);
//             }
//         }
//     }
// }
// 
// Path: src/main/java/com/superzanti/serversync/util/PrettyCollection.java
// public class PrettyCollection {
//     public static String get(Map<?, ?> map) {
//         String body = map.entrySet().stream()
//                       .map(entry -> String.format("%s -> %s", entry.getKey(), entry.getValue()))
//                       .collect(Collectors.joining("\n  "));
//         return build(body);
//     }
// 
//     public static String get(List<?> list) {
//         String body = list.stream()
//                        .map(Object::toString)
//                        .collect(Collectors.joining("\n  "));
//         return build(body);
//     }
// 
//     private static void appendHeader(StringBuilder builder) {
//         builder.append("{");
//         builder.append("\n  ");
//     }
// 
//     private static void appendFooter(StringBuilder builder) {
//         builder.append("\n");
//         builder.append("}");
//     }
// 
//     private static String build(String body) {
//         StringBuilder builder = new StringBuilder();
//         appendHeader(builder);
//         builder.append(body);
//         appendFooter(builder);
//         return builder.toString();
//     }
// }


import com.superzanti.serversync.files.FileManifest;
import com.superzanti.serversync.util.Logger;
import com.superzanti.serversync.util.PrettyCollection;
import java.io.Closeable;
import java.util.List;
import java.util.concurrent.Callable;
import java.util.function.Consumer;
     * <p>
     * This method requires an address and port to be configured via setAddress & setPort.
     *
     * @return A list of actions
     */
    public Callable<List<ActionEntry>> fetchActions() {
        return () -> {
            FileManifest manifest = sync.fetchManifest();
            Logger.log("Determining actions for manifest");
            Logger.log(PrettyCollection.get(manifest.directories));
            Logger.log(PrettyCollection.get(manifest.files));
            return sync.generateActionList(manifest);
        };
    }

    public Callable<Void> executeActions(List<ActionEntry> actions, Consumer<ActionProgress> progressConsumer) {
        return () -> {
            Logger.log(String.format("Executing actions: %s", PrettyCollection.get(actions)));
            sync.executeActionList(actions, progressConsumer);
            return null;
        };
    }

    public void connect() throws Exception {
        if (address == null || port == -1) {
            // TODO use a real exception here
            throw new Exception("Attempted to call connect without an address or port configured");
        }
        Client client = new Client(address, port);
        this.server = Server.forClient(client);
// Path: src/main/java/com/superzanti/serversync/files/FileManifest.java
// public class FileManifest implements Serializable {
//     public List<DirectoryEntry> directories = new ArrayList<>();
//     /**
//      * The files that the server wants to manage.
//      */
//     public List<FileEntry> files = new ArrayList<>();
// }
// 
// Path: src/main/java/com/superzanti/serversync/util/Logger.java
// public class Logger {
//     public static LoggerInstance instance = null;
//     private static final Object mutex = new Object();
//     private static Handler uiHandler;
// 
//     // Probably a heinous implementation of debounce but whatever
//     private static final long dbTimeMS = 2000L;
//     private static final ScheduledExecutorService dbRunner = Executors.newSingleThreadScheduledExecutor();
//     private static ScheduledFuture<?> waitingFlush;
// 
//     public static String getContext() {
//         return ServerSync.MODE == null ? "undefined" : ServerSync.MODE.toString();
//     }
// 
//     public static LoggerInstance getInstance() {
//         LoggerInstance result = instance;
//         if (result == null) {
//             //synchronized block to remove overhead
//             synchronized (mutex) {
//                 result = instance;
//                 if (result == null) {
//                     // if instance is null, initialize
//                     instance = result = new LoggerInstance(getContext());
//                 }
//             }
//         }
//         return result;
//     }
// 
//     public static void instantiate(String context) {
//         instance = new LoggerInstance(context);
//     }
// 
//     public static synchronized void setSystemOutput(boolean output) {
//         // enable/disable System.out logging
//         getInstance().javaLogger.setUseParentHandlers(output);
//     }
// 
//     public static synchronized void log(String s) {
//         getInstance().log(s);
//     }
// 
//     public static synchronized void error(String s) {
//         getInstance().error(s);
//     }
// 
//     public static synchronized void debug(String s) {
//         getInstance().debug(s);
//     }
// 
//     public static synchronized void debug(Exception e) {
//         getInstance().debug(Arrays.toString(e.getStackTrace()));
//     }
// 
//     public static synchronized void outputError(Object object) {
//         getInstance().debug("Failed to write object (" + object + ") to output stream");
//     }
// 
//     public static synchronized void attachUIHandler(Handler handler) {
//         if (uiHandler != null) {
//             uiHandler.close();
//             getInstance().javaLogger.removeHandler(uiHandler);
//         }
//         uiHandler = handler;
//         getInstance().javaLogger.addHandler(uiHandler);
//     }
// 
//     public static synchronized void flush() {
//         if (uiHandler != null) {
//             if (waitingFlush == null || waitingFlush.isDone()) {
//                 waitingFlush = dbRunner.schedule(() -> {
//                     uiHandler.flush();
//                 }, dbTimeMS, TimeUnit.MILLISECONDS);
//             }
//         }
//     }
// }
// 
// Path: src/main/java/com/superzanti/serversync/util/PrettyCollection.java
// public class PrettyCollection {
//     public static String get(Map<?, ?> map) {
//         String body = map.entrySet().stream()
//                       .map(entry -> String.format("%s -> %s", entry.getKey(), entry.getValue()))
//                       .collect(Collectors.joining("\n  "));
//         return build(body);
//     }
// 
//     public static String get(List<?> list) {
//         String body = list.stream()
//                        .map(Object::toString)
//                        .collect(Collectors.joining("\n  "));
//         return build(body);
//     }
// 
//     private static void appendHeader(StringBuilder builder) {
//         builder.append("{");
//         builder.append("\n  ");
//     }
// 
//     private static void appendFooter(StringBuilder builder) {
//         builder.append("\n");
//         builder.append("}");
//     }
// 
//     private static String build(String body) {
//         StringBuilder builder = new StringBuilder();
//         appendHeader(builder);
//         builder.append(body);
//         appendFooter(builder);
//         return builder.toString();
//     }
// }


// Path: src/main/java/com/superzanti/serversync/client/ClientWorker.java
import com.superzanti.serversync.files.FileManifest;
import com.superzanti.serversync.util.Logger;
import com.superzanti.serversync.util.PrettyCollection;
import java.io.Closeable;
import java.util.List;
import java.util.concurrent.Callable;
import java.util.function.Consumer;

     * <p>
     * This method requires an address and port to be configured via setAddress & setPort.
     *
     * @return A list of actions
     */
    public Callable<List<ActionEntry>> fetchActions() {
        return () -> {
            FileManifest manifest = sync.fetchManifest();
            Logger.log("Determining actions for manifest");
            Logger.log(PrettyCollection.get(manifest.directories));
            Logger.log(PrettyCollection.get(manifest.files));
            return sync.generateActionList(manifest);
        };
    }

    public Callable<Void> executeActions(List<ActionEntry> actions, Consumer<ActionProgress> progressConsumer) {
        return () -> {
            Logger.log(String.format("Executing actions: %s", PrettyCollection.get(actions)));
            sync.executeActionList(actions, progressConsumer);
            return null;
        };
    }

    public void connect() throws Exception {
        if (address == null || port == -1) {
            // TODO use a real exception here
            throw new Exception("Attempted to call connect without an address or port configured");
        }
        Client client = new Client(address, port);
        this.server = Server.forClient(client);
        if (!server.connect()) {