public class Control {
    private static volatile boolean firstStart = true;
    private static volatile boolean running = false;
    private static volatile Bot bot = NullBot.INSTANCE;

    private Control() {}

    public static Bot bot() {
        return bot;
    }

    /**
     * Sets the bot to the given value. Fails if bot is running.
     *
     * @param bot A {@link Bot} to set as the bot
     * @return {@code true} if successfully set new value for bot
     */
    public static synchronized boolean setBot(Bot bot) {
        if (running) {
            return false;
        }
        Control.bot = bot;
        return true;
    }

    /**
     * Restarts the bot. Works whether or not bot is currently running.
     *
     * @return {@code true} if bot started successfully, {@code false} if it failed to start
     */
    public static synchronized boolean restart() {
        shutdown(true);
        try {
            return startup();
        } catch (StartupException e) {
            App.logger.catching(e);
            return false;
        }
    }

    /**
     * Does what it says on the tin
     */
    public static void shutdownAndExit() {
        shutdown(false);
        App.logger.info("Process stopped");
        System.exit(0);
    }

    /**
     * Stops the bot and cleans up anything which needs to be cleaned up
     * before the bot is started again
     *
     * @param cleanup whether or not to cleanup various data with the
     *                expectation that the bot will be started again
     */
    public static synchronized boolean shutdown(boolean cleanup) {
        if (!running) {
            return false;
        }

        App.logger.info("Shutting down bot");
        Bot oldBot = bot;
        bot = NullBot.INSTANCE; // Quickly make calls to getBot() return NullBot
        oldBot.shutdown();
        if (cleanup) {
            shutdownCleanup(oldBot);
        }
        running = false;
        return true;
    }

    private static void shutdownCleanup(Bot bot) {
        clearCaches();
        // TODO unload libs?
    }

    /**
     * Starts the bot
     *
     * @return true if bot started successfully, false if bot was already running
     * @throws StartupException if failed to create bot properly
     */
    public static synchronized boolean startup() throws StartupException {
        if (running) {
            return false;
        }

        loadConfigs();

        if (firstStart) {
            firstStart = false;
            LibsLoader.load();
        } else {
            CommandResponseParser.reRegisterTerms();
        }

        init();
        try {
            createBot();
            running = true;
        } catch (BotInitException e) {
            throw new StartupException("Failed to start bot", e);
        }
        return true;
    }

    private static void clearCaches() {
        CommandScriptProcessor.clearScriptCache();
        FilterProcessor.clearScriptCache();
    }

    private static void createBot() throws BotInitException {
        // Connect to service
        Service service = Configs.getGeneralConfig().getExactlyAsOptional(GeneralConfig::getService)
                .orElseThrow(() -> new BotInitException("Failed to get service"));
        switch (service) {
            case TWITCH:
                bot = new TwitchBot();
                break;
            case BEAM:
                bot = new BeamBot();
                break;
            default:
                throw new BotInitException("Unknown service: " + service);
        }
        ThreadUtil.newSingleThreadExecutor("Bot").execute(() -> {
            try {
                App.logger.info("Bot Started");
                bot.startBot();
                App.logger.info("Bot Stopped");
            } catch (BotInitException e) {
                App.logger.catching(e);
            }
            handlePossibleFailedStartup();
        });
    }

    private static synchronized void handlePossibleFailedStartup() {
        if (!bot.isConnected()) {
            shutdown(true);
        }
    }

    private static void init() {
        loadPreloads(LoadStrategy.OVERWRITE);
        TermLoader.loadTerms();
        // TODO load filters (scripts)
    }

    public static void loadPreloads(LoadStrategy strategy) {
        // TODO stream over Base when filters are ready
        PreloadLoader.loadDirectory(Base.CMD, Chan.ALL, null, strategy);
        PreloadLoader.loadDirectory(Base.ALIAS, Chan.ALL, null, strategy);
        PreloadLoader.loadDirectory(Base.CMD, Chan.BOT, null, strategy);
        PreloadLoader.loadDirectory(Base.ALIAS, Chan.BOT, null, strategy);

        PreloadLoader.loadDirectoryForEachChannel(Base.CMD, strategy);
        PreloadLoader.loadDirectoryForEachChannel(Base.ALIAS, strategy);
    }

    private static void loadConfigs() {
        Configs.getGeneralConfig().updateAndAwait();
        Configs.reloadAccount();
    }

    public static class Graphics {
        private static boolean withGui = true;

        public static void useGui(boolean useGui) {
            withGui = useGui;
        }

        public static boolean present() {
            return withGui && !GraphicsEnvironment.isHeadless();
        }
    }

    public static class StartupException extends Exception {
        private StartupException(String message, Throwable cause) {
            super(message, cause);
        }
    }
} public class CmdParser {

    public static final String CLEAR = "clear";
    public static final String EXEC = "exec";
    public static final String JOINCHANNEL = "join";
    public static final String LEAVECHANNEL = "leave";
    public static final String OPEN_WEB = "open-web";
    public static final String QUIT = "quit";
    public static final String RESET = "reset";
    public static final String RESTART = "restart";
    public static final String START = "start";
    public static final String STOP = "stop";
    public static final String HELP = "help";
    private static final HashMap<String, CliCommand> map = new HashMap<>();
    private static final CliCommand.Builder commandBuilder = new CliCommand.Builder();
    private static final ExecutorService EXECUTOR_SERVICE = ThreadUtil.newSingleThreadExecutor("CLI Command Processor");
    private static List<String> args;
    private static String source = "";


    public static class ClearTargets {
        public static final String CLI = "cli";
        public static final String HISTORY = "history";
        public static final String INFO = "info";
        public static final String LOG = "log";
        public static final String WINDOWS = "windows";
        public static final Set<String> targets;

        static {
            Set<String> set = new HashSet<>();
            set.add(CLI);
            set.add(HISTORY);
            set.add(INFO);
            set.add(LOG);
            set.add(WINDOWS);
            targets = Collections.unmodifiableSet(set);
        }
    }

    static {
        // Add CLI commands
        if (Control.Graphics.present()) {
            initClear();
        }
        initExec();
        initJoinChannel();
        initLeaveChannel();
        initOpenWeb();
        initQuit();
        initReset();
        initRestart();
        initStart();
        initStop();
        initHelp();
    }

    private CmdParser() {}

    public static Set<String> getCommands() {
        return Collections.unmodifiableSet(map.keySet());
    }

    /**
     *
     * @param line a line for the CLI to process
     * @param source a {@link String} representing the source of the line, to which any delayed output
     *               generated by the {@link CliCommand} executed will be sent. (For example, if the
     *               {@link CliCommand} runs a {@link com.github.otbproject.otbproject.command.Command},
     *               the output of the {@code Command} will be sent to there.)
     * @param consumer a {@link Consumer} to handle the immediate {@link String} response of the CLI
     * @param afterProcessing a {@link Runnable} to execute after processing the line and consuming its
     *                        response, regardless of whether the processing or consuming encountered an
     *                        {@link Exception}
     */
    public static synchronized void processLineAndThen(String line, String source, Consumer<String> consumer, Runnable afterProcessing) {
        EXECUTOR_SERVICE.execute(() -> consumer.accept(processLine(line, source)));
        EXECUTOR_SERVICE.execute(afterProcessing);
    }

    private static String processLine(String line, String source) {
        CmdParser.source = source;
        String response = doLine(line);
        if (!response.isEmpty()) {
            App.logger.debug(response);
        }
        return response;
    }

    private static String doLine(String line) {
        args = Stream.of(line.trim().split(" ")).filter(s -> !s.isEmpty()).collect(Collectors.toList());
        if (!args.isEmpty()) {
            App.logger.debug("Processing input line: " + line);
            String cliCommand = args.get(0).toLowerCase();
            args = args.subList(1, args.size());
            if (map.containsKey(cliCommand)) {
                return map.get(cliCommand).get();
            } else {
                return printHelpNoCommand(cliCommand);
            }
        } else {
            App.logger.warn("Empty or invalid line. Unable to process.");
        }
        return "";
    }

    static String printHelpNoCommand(String term) {
        return term + ": command not found";
    }

    private static void initClear() {
        commandBuilder.withShortHelp("clear TARGET")
                .withLongHelp("Clears the specified window(s) or the CLI command history\n"
                        + "Valid targets are: " + ClearTargets.targets.stream().sorted().collect(Collectors.joining(", ")))
                .withAction(() -> {
                    if (args.isEmpty()) {
                        return "Not enough args for '" + CLEAR + "'";
                    }

                    switch (args.get(0)) {
                        case ClearTargets.CLI:
                            GuiApplication.clearCliOutput();
                            break;
                        case ClearTargets.LOG:
                            GuiApplication.clearLog();
                            break;
                        case ClearTargets.INFO:
                            GuiApplication.clearInfo();
                            break;
                        case ClearTargets.WINDOWS:
                            GuiApplication.clearCliOutput();
                            GuiApplication.clearLog();
                            GuiApplication.clearInfo();
                            break;
                        case ClearTargets.HISTORY:
                            GuiApplication.clearHistory();
                            break;
                        default:
                            return "Invalid target to clear: " + args.get(0) + "\nValid targets are: " + ClearTargets.targets.stream().sorted().collect(Collectors.joining(", "));
                    }
                    return "";
                });
        map.put(CLEAR, commandBuilder.create());
    }

    private static void initExec() {
        commandBuilder.withShortHelp("exec CHANNEL COMMAND")
                .withLongHelp("Runs the command denoted by COMMAND in the channel denoted by CHANNEL")
                .withAction(() -> {
                    if (args.size() < 2) {
                        return "Not enough args for '" + EXEC + "'";
                    }
                    String channelName = args.get(0).toLowerCase();
                    Optional<ChannelProxy> optional = Control.bot().channelManager().get(channelName);
                    if (!optional.isPresent()) {
                        return "Not in channel: " + channelName;
                    }
                    UserLevel ul = UserLevel.INTERNAL;
                    String command = args.get(1);
                    for (int i = 2; i < args.size(); i++) {
                        command += " ";
                        command += args.get(i);
                    }
                    String destinationChannel = InternalMessageSender.DESTINATION_PREFIX + source;
                    PackagedMessage packagedMessage = new PackagedMessage(command, destinationChannel, channelName, destinationChannel, ul, MessagePriority.DEFAULT);
                    optional.get().receiveMessage(packagedMessage);
                    return "Command output above.";
                });
        map.put(EXEC, commandBuilder.create());
    }

    private static void initJoinChannel() {
        commandBuilder.withShortHelp("join CHANNEL")
                .withLongHelp("Makes the bot join the channel denoted by CHANNEL")
                .withAction(() -> {
                    if (args.size() > 0) {
                        String channel  = args.get(0).toLowerCase();
                        boolean success = Control.bot().channelManager().join(channel, EnumSet.of(JoinCheck.IS_CHANNEL));
                        if (success) {
                            ChannelJoinSetting channelJoinSetting = Configs.getBotConfig().get(BotConfig::getChannelJoinSetting);
                            if (channelJoinSetting == ChannelJoinSetting.WHITELIST) {
                                Configs.getBotConfig().edit(config -> config.getWhitelist().add(channel));
                            } else if (channelJoinSetting == ChannelJoinSetting.BLACKLIST) {
                                Configs.getBotConfig().edit(config -> config.getBlacklist().remove(channel));
                            }
                        }
                        String string = success ? "Successfully joined" : "Failed to join";
                        return string + " channel: " + channel;
                    } else {
                        return "Not enough args for '" + JOINCHANNEL + "'";
                    }
                });
        map.put(JOINCHANNEL, commandBuilder.create());
    }

    private static void initLeaveChannel() {
        commandBuilder.withShortHelp("leave CHANNEL")
                .withLongHelp("Makes the bot leave the channel denoted by CHANNEL")
                .withAction(() -> {
                    if (args.size() > 0) {
                        boolean success = Control.bot().channelManager().leave(args.get(0).toLowerCase());
                        String string = success ? "Successfully left" : "Failed to leave";
                        return string + " channel: " + args.get(0).toLowerCase();
                    } else {
                        return "Not Enough Args for '" + LEAVECHANNEL + "'";
                    }
                });
        map.put(LEAVECHANNEL, commandBuilder.create());
    }

    private static void initOpenWeb() {
        commandBuilder.withShortHelp("open-web")
                .withLongHelp("Opens the web interface in the default browser")
                .withAction(() -> {
                    WebInterface.openInBrowser();
                    return "";
                });
        map.put(OPEN_WEB, commandBuilder.create());
    }

    private static void initQuit() {
        commandBuilder.withShortHelp("quit")
                .withLongHelp("Stops the bot and exits")
                .withAction(() -> {
                    Control.shutdownAndExit();
                    return "";
                });
        map.put(QUIT, commandBuilder.create());
    }

    private static void initReset() {
        commandBuilder.withShortHelp("reset [CHANNEL]")
                .withLongHelp("Resets all commands and aliases to their defaults. Either for CHANNEL, or if not specified, all channels.")
                .withAction(() -> {
                    if (args.size() > 0) {
                        PreloadLoader.loadDirectory(Base.CMD, Chan.SPECIFIC, args.get(0).toLowerCase(), LoadStrategy.FROM_LOADED);
                        PreloadLoader.loadDirectory(Base.CMD, Chan.ALL, args.get(0).toLowerCase(), LoadStrategy.FROM_LOADED);
                        PreloadLoader.loadDirectory(Base.ALIAS, Chan.SPECIFIC, args.get(0).toLowerCase(), LoadStrategy.FROM_LOADED);
                        PreloadLoader.loadDirectory(Base.ALIAS, Chan.ALL, args.get(0).toLowerCase(), LoadStrategy.FROM_LOADED);
                    } else {
                        PreloadLoader.loadDirectory(Base.CMD, Chan.ALL, null, LoadStrategy.FROM_LOADED);
                        PreloadLoader.loadDirectory(Base.ALIAS, Chan.ALL, null, LoadStrategy.FROM_LOADED);
                        PreloadLoader.loadDirectoryForEachChannel(Base.CMD, LoadStrategy.FROM_LOADED);
                        PreloadLoader.loadDirectoryForEachChannel(Base.ALIAS, LoadStrategy.FROM_LOADED);
                    }
                    return "Reset complete";
                });
        map.put(RESET, commandBuilder.create());
    }

    private static void initRestart() {
        commandBuilder.withShortHelp("restart")
                .withLongHelp("Restarts the bot")
                .withAction(() -> Control.restart() ? "Restarted bot" : "Failed to restart bot");
        map.put(RESTART, commandBuilder.create());
    }

    private static void initStart() {
        commandBuilder.withShortHelp("start")
                .withLongHelp("Starts the bot")
                .withAction(() -> {
                    try {
                        return (Control.startup() ? "Started bot" : "Did not start bot - bot already running");
                    } catch (Control.StartupException e) {
                        App.logger.catching(e);
                        return "Failed to start bot";
                    }
                });
        map.put(START, commandBuilder.create());
    }

    private static void initStop() {
        commandBuilder.withShortHelp("stop")
                .withLongHelp("Stops the bot")
                .withAction(() -> (Control.shutdown(true) ? "Stopped bot" : "Bot not running"));
        map.put(STOP, commandBuilder.create());
    }

    private static void initHelp() {
        commandBuilder.withShortHelp("help [CLI_COMMAND]")
                .withLongHelp("Prints the help message for the cli command denoted by CLI_COMMAND, or if not specified will list all the cli commands")
                .withAction(() -> {
                    String responseStr;
                    if (args.isEmpty()) {
                        responseStr = map.keySet().stream().filter(key -> !key.equals(HELP)).map(key -> map.get(key).getShortHelp()).sorted().collect(Collectors.joining("\n"));
                        responseStr += "\n" + map.get(HELP).getShortHelp();
                        responseStr += "\n\nType \"help <command>\" to print the help message for a cli command.";
                    } else {
                        String arg = args.get(0);
                        if (map.keySet().contains(arg)) {
                            responseStr = map.get(arg).getFullHelp();
                        } else {
                            return printHelpNoCommand(arg);
                        }
                    }
                    return responseStr;
                });
        map.put(HELP, commandBuilder.create());
    }
} public class FSUtil {
    public static final String ERROR_MSG = "Failed to create directory: ";
    private static final String BASE_DIR_NAME = ".otbproject";
    public static final String BASE_DIR_DEFAULT = System.getProperty("user.home") + File.separator + BASE_DIR_NAME;
    private static final String ASSETS_DIR_NAME = "assets";
    private static final String ALIASES_DIR_NAME = "aliases";
    private static final String COMMANDS_DIR_NAME = "commands";
    private static final String CONFIG_DIR_NAME = "config";
    private static final String DATA_DIR_NAME = "data";
    private static final String FILTERS_DIR_NAME = "filters";
    private static final String FILTER_GROUPS_DIR_NAME = "filter-groups";
    private static final String LOGS_DIR_NAME = "logs";
    private static final String SCRIPT_DIR_NAME = "scripts";
    private static final String LIBS_DIR_NAME = "libs";
    private static final String TERMS_DIR_NAME = "terms";
    private static final String WEB_DIR_NAME = "web";

    public static final String GUI_HISTORY_FILE = "gui-command-history.json";

    private static String baseDir = BASE_DIR_DEFAULT;

    private FSUtil() {}

    public static String getBaseDir() {
        return baseDir;
    }

    // Assumes path does not have a trailing slash
    public static void setBaseDirPath(String path) {
        baseDir = path + File.separator + BASE_DIR_NAME;
    }

    public static String assetsDir() {
        return baseDir + File.separator + ASSETS_DIR_NAME;
    }

    public static String aliasesDir() {
        return baseDir + File.separator + ALIASES_DIR_NAME;
    }

    public static String commandsDir() {
        return baseDir + File.separator + COMMANDS_DIR_NAME;
    }

    public static String configDir() {
        return baseDir + File.separator + CONFIG_DIR_NAME;
    }

    public static String dataDir() {
        return baseDir + File.separator + DATA_DIR_NAME;
    }

    public static String channelDataDir(String channel) {
        return dataDir() + File.separator + DirNames.CHANNELS + File.separator + channel;
    }

    private static String filtersBaseDir() {
        return baseDir + File.separator + FILTERS_DIR_NAME;
    }

    public static String filtersDir() {
        return filtersBaseDir() + File.separator + FILTERS_DIR_NAME;
    }

    public static String filterGroupsDir() {
        return filtersBaseDir() + File.separator + FILTER_GROUPS_DIR_NAME;
    }

    public static String logsDir() {
        return baseDir + File.separator + LOGS_DIR_NAME;
    }

    public static String scriptDir() {
        return baseDir + File.separator + SCRIPT_DIR_NAME;
    }

    public static String commandScriptDir() {
        return scriptDir() + File.separator + COMMANDS_DIR_NAME;
    }

    public static String scriptLibsDir() {
        return scriptDir() + File.separator + LIBS_DIR_NAME;
    }

    public static String termScriptDir() {
        return scriptDir() + File.separator + TERMS_DIR_NAME;
    }

    public static String filterScriptDir() {
        return scriptDir() + File.separator + FILTERS_DIR_NAME;
    }

    public static String webDir() {
        return baseDir + File.separator + WEB_DIR_NAME;
    }

    public static Stream<File> streamDirectory(File directory) {
        return streamDirectory(directory, "Error listing files for directory: " + directory.getPath());
    }

    public static Stream<File> streamDirectory(File directory, String errMsg) {
        File[] files = directory.listFiles();
        if (files == null) {
            App.logger.error(errMsg);
            return Stream.empty();
        }
        return Stream.of(files);
    }

    public static class DirNames {
        public static final String ALL_CHANNELS = "all-channels";
        public static final String BOT_CHANNEL = "bot-channel";
        public static final String CHANNELS = "channels";

        public static final String LOADED = "loaded";
        public static final String TO_LOAD = "to-load";
        public static final String FAILED = "failed";
    }

    public static class DatabaseNames {
        public static final String MAIN = "main.db";
        public static final String QUOTES = "quotes.db";
    }

    public static class ConfigFileNames {
        public static final String ACCOUNT_TWITCH = "account-twitch.json";
        public static final String ACCOUNT_BEAM = "account-beam.json";
        public static final String GENERAL_CONFIG = "general-config.json";
        public static final String BOT_CONFIG = "bot-config.json";
        public static final String CHANNEL_CONFIG = "config.json";
        public static final String WEB_CONFIG = "web-config.json";
    }

    public static class Assets {
        public static final String LOGO = "logo.png";
    }
} public enum Chan {
    BOT, ALL, SPECIFIC
} public class GuiApplication extends Application {
    private static GuiController controller;

    /**
     * Variable set to {@code true} when {@code GuiApplication} has
     * loaded and dereferrencing {@code controller} will not produce
     * a {@link NullPointerException}
     */
    private static volatile boolean ready = false;
    /**
     * Not volatile to allow faster access than to {@code ready}
     * for most of the execution, as this value is not volatile
     */
    private static boolean cheapReady = false;
    static boolean notReady() {
        return !cheapReady && !ready;
    }

    static final Lock READY_LOCK = new ReentrantLock(true);
    static final BlockingQueue<Runnable> NOT_READY_QUEUE = new LinkedBlockingQueue<>();

    private static final ExecutorService DESKTOP_SERVICE = ThreadUtil.newSingleThreadExecutor("Desktop Daemon");

    /**
     * The main entry point for all JavaFX applications.
     * The start method is called after the init method has returned,
     * and after the system is ready for the application to begin running.
     * <p>
     * NOTE: This method is called on the JavaFX Application Thread.
     * </p>
     *
     * @param primaryStage the primary stage for this application, onto which
     *                     the application scene can be set. The primary stage will be embedded in
     *                     the browser if the application was launched as an applet.
     *                     Applications may create other stages, if needed, but they will not be
     *                     primary stages and will not be embedded in the browser.
     */
    @Override
    public void start(Stage primaryStage) throws Exception {
        Thread.currentThread().setUncaughtExceptionHandler(ThreadUtil.UNCAUGHT_EXCEPTION_HANDLER);
        Font.loadFont(getClass().getClassLoader().getResourceAsStream("assets/fonts/UbuntuMono-R.ttf"), 12);
        Font.loadFont(getClass().getClassLoader().getResourceAsStream("assets/fonts/Ubuntu-R.ttf"), 12);
        FXMLLoader loader = new FXMLLoader(getClass().getClassLoader().getResource("console.fxml"));
        Parent start = loader.load();
        primaryStage.setScene(new Scene(start, 1200, 515));
        primaryStage.setResizable(false);
        primaryStage.setTitle("OTB");
        primaryStage.getIcons().add(new Image("file:" + FSUtil.assetsDir() + File.separator + FSUtil.Assets.LOGO));
        // Create tailer
        CustomTailerListenerAdapter listenerAdapter = new CustomTailerListenerAdapter();
        File logFile = new File(FSUtil.logsDir() + File.separator + "console.log");
        Tailer tailer = Tailer.create(logFile, listenerAdapter, 250);
        // Set on-close action
        primaryStage.setOnCloseRequest(event -> {
            Alert alert = new Alert(Alert.AlertType.WARNING);
            alert.setTitle("Confirm Close");
            alert.setHeaderText("WARNING: \"Close Window\" DOES NOT STOP THE BOT.");
            alert.setContentText("Closing this window without exiting may make it difficult to stop the bot.\nPress \"Exit\" to stop the bot and exit.\nPress \"Cancel\" to keep the window open.");
            DialogPane dialogPane = alert.getDialogPane();
            GuiUtils.setDialogPaneStyle(dialogPane);
            ButtonType buttonTypeCloseNoExit = new ButtonType("Close Window", ButtonBar.ButtonData.LEFT);
            ButtonType buttonTypeExit = new ButtonType("Exit", ButtonBar.ButtonData.FINISH);
            ButtonType buttonTypeCancel = new ButtonType("Cancel", ButtonBar.ButtonData.FINISH);
            alert.getButtonTypes().setAll(buttonTypeCloseNoExit, buttonTypeExit, buttonTypeCancel);
            GuiUtils.setDefaultButton(alert, buttonTypeExit);
            alert.initStyle(StageStyle.UNDECORATED);
            alert.showAndWait().ifPresent(buttonType -> {
                if (buttonType == buttonTypeCloseNoExit) {
                    primaryStage.hide();
                    tailer.stop();
                    listenerAdapter.stop();
                } else if (buttonType == buttonTypeExit) {
                    Control.shutdownAndExit();
                    System.exit(0);
                }
            });
            event.consume();
        });
        controller = loader.<GuiController>getController();
        setUpMenus();
        controller.cliOutput.appendText(">  ");
        controller.commandsInput.setEditable(false);
        controller.commandsOutput.appendText("Type \"help\" for a list of commands.\nThe PID of the bot is probably "
                + App.PID  + " if you are using an Oracle JVM, but it may be different,"
                + " especially if you are using a different JVM. Be careful stopping the bot using this PID.");
        controller.readHistory();
        primaryStage.show();

        // Notify waiting GUI Runnables that ready
        READY_LOCK.lock();
        try {
            cheapReady = true;
            ready = true;
            NOT_READY_QUEUE.forEach(Runnable::run);
            NOT_READY_QUEUE.clear();
        } finally {
            READY_LOCK.unlock();
        }
    }

    public static void start(String[] args) {
        launch(args);
    }

    public static void setInputInactive() {
        GuiUtils.runSafe(() -> {
            controller.commandsInput.setEditable(false);
            controller.commandsInput.setPromptText("Command executing, please wait...");
        });
    }

    public static void setInputActive() {
        GuiUtils.runSafe(() -> {
            controller.commandsInput.setEditable(true);
            controller.commandsInput.setPromptText("Enter command here...");
        });
    }

    public static void addInfo(String text) {
        GuiUtils.runSafe(() -> controller.commandsOutput.appendText("\n\n" + text));
    }

    public static void clearLog() {
        GuiUtils.runSafe(controller.logOutput::clear);
    }

    public static void clearInfo() {
        GuiUtils.runSafe(controller.commandsOutput::clear);
    }

    public static void clearCliOutput() {
        GuiUtils.runSafe(controller.cliOutput::clear);
    }

    public static void clearHistory() {
        GuiUtils.runSafe(() -> {
            controller.history.clear();
            controller.historyPointer = 0;
            controller.writeHistory();
        });
    }

    static class CustomTailerListenerAdapter extends TailerListenerAdapter {
        private final BlockingQueue<String> queue = new LinkedBlockingQueue<>();
        private final List<String> buffer = new ArrayList<>();
        private final ScheduledFuture<?> scheduledFuture;

        public CustomTailerListenerAdapter() {
            scheduledFuture = Executors.newSingleThreadScheduledExecutor(ThreadUtil.newThreadFactory("GUI-console-daemon"))
                    .scheduleWithFixedDelay(this::addToConsole, 0, 100, TimeUnit.MILLISECONDS);
        }

        void stop() {
            scheduledFuture.cancel(true);
        }

        private void addToConsole() {
            // Ensure it does not attempt to append text before the GUI is ready
            if (notReady()) {
                return;
            }

            queue.drainTo(buffer);
            if (!buffer.isEmpty()) {
                String text = buffer.stream().collect(Collectors.joining("\n", "", "\n"));
                GuiUtils.runSafe(() -> controller.logOutput.appendText(text));
                buffer.clear();
            }
        }

        @Override
        public void handle(String line) {
            try {
                queue.put(line);
            } catch (InterruptedException e) {
                App.logger.catching(e);
                Thread.currentThread().interrupt();
            }
        }
    }

    private void setUpMenus() {
        controller.openBaseDir.setOnAction(event -> {
            DESKTOP_SERVICE.execute(() -> {
                try {
                    Desktop.getDesktop().open(new File(FSUtil.getBaseDir()));
                } catch (IOException e) {
                    App.logger.catching(e);
                }
            });
            event.consume();
        });
        controller.quit.setOnAction(event -> {
            Alert alert = new Alert(Alert.AlertType.CONFIRMATION);
            alert.setTitle("Confirm Quit");
            alert.setHeaderText("Are you sure you want to quit?");
            DialogPane dialogPane = alert.getDialogPane();
            GuiUtils.setDialogPaneStyle(dialogPane);
            ButtonType buttonTypeYes = new ButtonType("Yes", ButtonBar.ButtonData.FINISH);
            ButtonType buttonTypeNo = new ButtonType("No", ButtonBar.ButtonData.FINISH);
            alert.getButtonTypes().setAll(buttonTypeYes, buttonTypeNo);
            GuiUtils.setDefaultButton(alert, buttonTypeYes);
            alert.initStyle(StageStyle.UNDECORATED);
            alert.showAndWait().ifPresent(buttonType -> {
                if (buttonType == buttonTypeYes) {
                    Control.shutdownAndExit();
                }
            });
            event.consume();
        });
        controller.botStart.setOnAction(event -> {
            try {
                addInfo(Control.startup() ? "Started bot" : "Did not start bot - bot already running");
            } catch (Control.StartupException e) {
                App.logger.catching(e);
                addInfo("Failed to start bot");
            }
            event.consume();
        });
        controller.botStop.setOnAction(event -> {
            addInfo(Control.shutdown(true) ? "Bot stopped" : "Did not stop bot - bot not running");
            event.consume();
        });
        controller.botRestart.setOnAction(event -> {
            addInfo(Control.restart() ? "Restarted bot" : "Failed to restart bot");
            event.consume();
        });
        controller.webOpen.setOnAction(event -> {
            openWebInterfaceInBrowser();
            event.consume();
        });
    }

    private void openWebInterfaceInBrowser() {
        this.getHostServices().showDocument("http://127.0.0.1:" + Configs.getWebConfig().get(WebConfig::getPortNumber));
    }

    public static void newReleaseAlert() {
        GuiUtils.runSafe(() -> {
            String url = "https://github.com/OTBProject/OTBProject/releases/latest";
            Alert alert = new Alert(Alert.AlertType.INFORMATION);
            alert.setTitle("New Release Available");
            alert.setHeaderText("New Release Available: OTB Version " + AppVersion.latest());
            alert.setContentText("Version " + AppVersion.latest() + " of OTB is now available!" +
                    "\n\nPress \"Get New Release\" or go to" +
                    "\n" + url +
                    "\nto get the new release." +
                    "\n\nPressing \"Don't Ask Again\" will prevent notifications " +
                    "\nfor all future releases of OTB.");
            DialogPane dialogPane = alert.getDialogPane();
            GuiUtils.setDialogPaneStyle(dialogPane);
            ButtonType buttonTypeDontAskAgain = new ButtonType("Don't Ask Again", ButtonBar.ButtonData.LEFT);
            ButtonType buttonTypeGetRelease = new ButtonType("Get New Release", ButtonBar.ButtonData.FINISH);
            ButtonType buttonTypeIgnoreOnce = new ButtonType("Ignore Once", ButtonBar.ButtonData.FINISH);
            alert.getButtonTypes().setAll(buttonTypeDontAskAgain, buttonTypeGetRelease, buttonTypeIgnoreOnce);
            GuiUtils.setDefaultButton(alert, buttonTypeGetRelease);
            alert.initStyle(StageStyle.UNDECORATED);
            alert.showAndWait().ifPresent(buttonType -> {
                if (buttonType == buttonTypeDontAskAgain) {
                    Configs.getGeneralConfig().edit(config -> config.setUpdateChecking(false));
                } else if (buttonType == buttonTypeGetRelease) {
                    DESKTOP_SERVICE.execute(() -> {
                        try {
                            Desktop.getDesktop().browse(new URI(url));
                        } catch (IOException | URISyntaxException e) {
                            App.logger.catching(e);
                        }
                    });
                }
            });
        });
    }

    public static void errorAlert(String title, String header) {
        GuiUtils.runSafe(() -> {
            Alert alert = new Alert(Alert.AlertType.ERROR);
            alert.setTitle(title);
            alert.setHeaderText(header);
            String url = "https://github.com/OTBProject/OTBProject/issues";

            FlowPane outer = new FlowPane();
            FlowPane inner = new FlowPane();
            Hyperlink link = new Hyperlink("here");
            link.setOnAction((evt) -> DESKTOP_SERVICE.execute(() -> {
                try {
                    Desktop.getDesktop().open(new File(FSUtil.logsDir()));
                } catch (IOException e) {
                    App.logger.catching(e);
                }
            }));
            outer.getChildren().addAll(new Label("Please report this problem to the developers at"), new Label('"' + url + '"'), inner,
                    new Label("(in the \"logs\" folder in your installation directory)"));
            inner.getChildren().addAll(new Label("and give them the log file \"app.log\" found"), link);
            alert.getDialogPane().contentProperty().set(outer);

            showErrorAlert(alert, url);
        });
    }

    public static void fatalErrorAlert(String fileName) {
        GuiUtils.runSafe(() -> {
            Alert alert = new Alert(Alert.AlertType.ERROR);
            alert.setTitle("Fatal Error");
            alert.setHeaderText("OTB experienced a fatal error the last time it ran");
            String url = "https://github.com/OTBProject/OTBProject/issues";
            alert.setContentText("Please report this problem to the developers at\n\"" + url + "\"\nand provide them with the file: " + fileName);
            showErrorAlert(alert, url);
        });
    }

    public static void multipleFatalErrorAlert(java.util.List<String> fileNames) {
        GuiUtils.runSafe(() -> {
            Alert alert = new Alert(Alert.AlertType.ERROR);
            alert.setTitle("Fatal Error");
            alert.setHeaderText("OTB somehow experienced multiple fatal errors the last time it ran");
            String url = "https://github.com/OTBProject/OTBProject/issues";
            alert.setContentText("Please report this problem to the developers at\n\"" + url + "\"\nand provide them with the files:\n"
                    + fileNames.stream().collect(Collectors.joining("\n")));
            showErrorAlert(alert, url);
        });
    }

    private static void showErrorAlert(Alert alert, String url) {
        DialogPane dialogPane = alert.getDialogPane();
        GuiUtils.setDialogPaneStyle(dialogPane);
        ButtonType buttonTypeReportIssue = new ButtonType("Report Problem", ButtonBar.ButtonData.FINISH);
        ButtonType buttonTypeClose = new ButtonType("OK", ButtonBar.ButtonData.FINISH);
        alert.getButtonTypes().setAll(buttonTypeReportIssue, buttonTypeClose);
        GuiUtils.setDefaultButton(alert, buttonTypeReportIssue);
        alert.initStyle(StageStyle.UNDECORATED);
        alert.showAndWait().ifPresent(buttonType -> {
            if (buttonType == buttonTypeReportIssue && Desktop.isDesktopSupported()) {
                DESKTOP_SERVICE.execute(() -> {
                    try {
                        Desktop.getDesktop().browse(new URI(url));
                    } catch (IOException | URISyntaxException e) {
                        App.logger.catching(e);
                    }
                });
            }
        });
    }
} public class InternalMessageSender {
    public static final String DESTINATION_PREFIX = "internal:";
    public static final String CLI = "cli";
    public static final String TERMINAL = "terminal";

    private InternalMessageSender() {}

    public static void send(String destination, String message, String source) {
        switch (destination) {
            case CLI:
                sendToCli(message, source);
                break;
            case TERMINAL:
                sendToTerminal(message, source);
                break;
            default:
                App.logger.error("Invalid internal destination: " + destination);
        }
    }

    private static void sendToCli(String message, String source) {
        App.logger.debug("Sending to CLI from <" + source + ">: " + message);
        GuiApplication.addInfo("<" + source + "> " + message);
    }

    private static void sendToTerminal(String message, String source) {
        System.out.println("<" + source + "> " + message);
    }
} public class FatalChecker {
    private FatalChecker() {}

    public static void checkForPreviousFatalCrashes() {
        List<String> fileList = FSUtil.streamDirectory(new File(System.getProperty("user.dir")))
                .filter(File::isFile)
                .map(File::getName)
                .filter(s -> s.startsWith("OTBProjectFatal-"))
                .collect(Collectors.toList());
        String path = FSUtil.dataDir() + File.separator + "fatal-crash-files.json";
        Optional<String[]> optional = JsonHandler.readValue(path, String[].class);
        List<String> previousList = Arrays.asList(optional.orElse(new String[0]));
        JsonHandler.writeValue(path, fileList);
        fileList.removeAll(previousList);

        if (fileList.size() == 1) {
            App.logger.error("OTB experienced a fatal error the last time it ran");
            String fileName = fileList.get(0);
            App.logger.error("Please report this problem to the developers, and provide them with the file: " + fileName);
            if (Control.Graphics.present()) {
                GuiApplication.fatalErrorAlert(fileName);
            }
        } else if (fileList.size() > 1) {
            App.logger.error("OTB somehow seems to have experienced multiple fatal errors the last time it ran. This should not be possible.");
            App.logger.error("Please report this problem to the developers, and provide them with the files: "
                    + fileList.stream().collect(Collectors.joining(", ")));
            if (Control.Graphics.present()) {
                GuiApplication.multipleFatalErrorAlert(fileList);
            }
        }
    }
} public class ThreadUtil {
    public static final Thread.UncaughtExceptionHandler UNCAUGHT_EXCEPTION_HANDLER;

    private ThreadUtil() {}

    static {
        UNCAUGHT_EXCEPTION_HANDLER = (t, e) -> {
            App.logger.error("Thread crashed: " + t.getName());
            App.logger.catching(e);
            Watcher.logException();
        };
        Thread.setDefaultUncaughtExceptionHandler(UNCAUGHT_EXCEPTION_HANDLER);
    }

    public static ExecutorService newSingleThreadExecutor() {
        return Executors.newSingleThreadExecutor(newThreadFactory());
    }

    public static ExecutorService newSingleThreadExecutor(String nameFormat) {
        return Executors.newSingleThreadExecutor(newThreadFactory(nameFormat));
    }

    public static ExecutorService newCachedThreadPool() {
        return Executors.newCachedThreadPool(newThreadFactory());
    }

    public static ExecutorService newCachedThreadPool(String nameFormat) {
        return Executors.newCachedThreadPool(newThreadFactory(nameFormat));
    }

    public static ThreadFactory newThreadFactory() {
        return new ThreadFactoryBuilder()
                .setUncaughtExceptionHandler(UNCAUGHT_EXCEPTION_HANDLER)
                .build();
    }

    public static ThreadFactory newThreadFactory(String nameFormat) {
        return new ThreadFactoryBuilder()
                .setNameFormat(nameFormat)
                .setUncaughtExceptionHandler(UNCAUGHT_EXCEPTION_HANDLER)
                .build();
    }

    public static void interruptIfInterruptedException(Exception e) {
        if (e instanceof InterruptedException) {
            Thread.currentThread().interrupt();
        }
    }
} public class VersionCompatHelper {
    private VersionCompatHelper() {}

    public static void urgentCompatFixes(Version oldVersion) {
        if (versionCheck(oldVersion)) {
            fixGeneralConfig();
        }
    }

    public static void normalCompatFixes(Version oldVersion) {
        if (versionCheck(oldVersion)) {
            fix1_1To2_0();
        }
    }

    private static boolean versionCheck(Version oldVersion) {
        return oldVersion.checker().major(1).minor(1).isVersion();
    }

    private static void fix1_1To2_0() {
        fixScripts();
        removeOldPreloads();
        removeOldVersionFile();
    }

    private static void fixScripts() {
        // Move scripts from base dir into subdirectory
        FSUtil.streamDirectory(new File(FSUtil.scriptDir()))
                .filter(File::isFile)
                .forEach(file -> file.renameTo(new File(FSUtil.commandScriptDir() + File.separator + file.getName())));
    }

    private static void fixGeneralConfig() {
        Optional<GeneralConfigOld> optional = JsonHandler.readValue((FSUtil.configDir() + File.separator + FSUtil.ConfigFileNames.GENERAL_CONFIG), GeneralConfigOld.class);
        if (!optional.isPresent()) {
            return;
        }
        GeneralConfigOld configOld = optional.get();
        GeneralConfig configNew = new GeneralConfig();

        configNew.setService(configOld.getServiceName());
        configNew.setPermanentlyEnabledCommands(configOld.permanently_enabled_commands);
        configNew.getPermanentlyEnabledCommands().add("!leave");
        JsonHandler.writeValue((FSUtil.configDir() + File.separator + FSUtil.ConfigFileNames.GENERAL_CONFIG), configNew);
    }

    private static void removeOldVersionFile() {
        deleteFile(FSUtil.configDir() + File.separator + "VERSION");
    }

    private static void removeOldPreloads() {
        String basePath = new PathBuilder().base(Base.CMD).channels(Chan.ALL).load(Load.ED).create();
        deleteFile(basePath + File.separator + "command.reset.count.success.json");
        deleteFile(basePath + File.separator + "reset-count.json");
        deleteFile(FSUtil.commandScriptDir() + File.separator + "ScriptResetCount.groovy");
        FSUtil.streamDirectory(new File(FSUtil.dataDir() + File.separator + FSUtil.DirNames.CHANNELS))
                .map(File::getName)
                .peek(s -> App.logger.debug("Deleting old commands for channel: " + s))
                .map(Databases::createChannelMainDbWrapper)
                .forEach(db -> {
                    removeCommand(db, "~%command.reset.count.success");
                    removeCommand(db, "!resetCount");
                });
    }

    private static void removeCommand(DatabaseWrapper db, String command) {
        if (Commands.remove(db, command)) {
            App.logger.debug("Successfully removed old command: " + command);
        } else {
            App.logger.warn("Failed to remove old command: " + command);
        }
    }

    private static void deleteFile(String path) {
        if (new File(path).delete()) {
            App.logger.info("Deleted " + path);
        } else {
            App.logger.error("Failed to delete " + path);
        }
    }
} public enum LoadStrategy {
    UPDATE, OVERWRITE, FROM_LOADED
} public class AppVersion {
    private static final Version CURRENT = getCurrentVersion();

    private AppVersion() {}

    private static class ReleaseDataHolder {
        private static final ImmutableList<ReleaseData> RELEASE_DATA;
        private static final Version LATEST_VERSION;
        
        static {
            RELEASE_DATA = fetchReleaseData();
            LATEST_VERSION = RELEASE_DATA.stream()
                    .map(ReleaseData::getVersion)
                    .max(Version::compareTo)
                    .orElse(Version.create(0, 0, 0, Version.Type.RELEASE));
        }
        
        private static ImmutableList<ReleaseData> fetchReleaseData() {
            try {
                URL url = new URL("https://otbproject.github.io/release-data/app");
                try (BufferedReader in = new BufferedReader(new InputStreamReader(url.openStream()))) {
                    String json = in.lines().collect(Collectors.joining("\n"));
                    Optional<ReleaseData[]> optional = JsonHandler.gsonFromJson(json, ReleaseData[].class);
                    if (optional.isPresent()) {
                        return ImmutableList.copyOf(optional.get());
                    }
                }
            } catch (IOException e) {
                App.logger.catching(e);
            }
            return ImmutableList.of();
        }
    }

    private static String getVersionString() {
        return AppVersion.class.getPackage().getImplementationVersion();
    }

    public static Version current() {
        return CURRENT;
    }

    private static Version getCurrentVersion() {
        Version version;
        try {
            version = Version.parseVersion(getVersionString());
        } catch (Version.ParseException e) {
            e.printStackTrace(); // because logger not initialized, and should also never fail in actual execution
            version = Version.create(0, 0, 0, Version.Type.RELEASE);
        }
        return version;
    }

    public static Version latest() {
        return ReleaseDataHolder.LATEST_VERSION;
    }

    public static ImmutableList<ReleaseData> getReleaseData() {
        return ReleaseDataHolder.RELEASE_DATA;
    }
} public class Versions {
    private static final String RATE_LIMIT_INFO_PATH = FSUtil.dataDir() + File.separator + "rate-limit-info.json";

    private Versions() {}

    public static Optional<Version> readFromFile(String path) {
        return readFromFile(new File(path));
    }

    public static Optional<Version> readFromFile(File file) {
        if (!file.exists()) {
            return Optional.empty();
        }
        try {
            BufferedReader fileReader = new BufferedReader(new FileReader(file));
            String versionStr = fileReader.readLine();
            fileReader.close();
            return Version.parseAsOptional(versionStr);
        } catch (IOException | Version.ParseException e) {
            App.logger.catching(e);
            return Optional.empty();
        }
    }

    public static boolean writeToFile(String path, Version version) {
        return writeToFile(new File(path), version);
    }

    public static boolean writeToFile(File path, Version version) {
        PrintStream ps;
        try {
            ps = new PrintStream(path);
        } catch (FileNotFoundException e) {
            App.logger.catching(e);
            return false;
        }
        ps.println(version);
        ps.close();
        return true;
    }

    /**
     * Get the version of the latest release from an OTB repo
     *
     * @param otbRepo the name of the repository
     * @return A {@link String} representing the version of the latest
     * release, or {@code null} if the version could not be determined
     */
    public static String lookupLatestGithubVersion(String otbRepo) {
        return getVersionFromJsonNodeOptional(getJsonForLatestGithubRelease(otbRepo));
    }


    public static String getVersionFromJsonNodeOptional(Optional<JsonNode> optional) {
        if (optional.isPresent()) {
            return optional.get().path("tag_name").textValue();
        }
        return null;
    }

    /**
     * Get the version of the latest release from an OTB repo
     *
     * @param otbRepo the name of the repository
     * @return An {@link Optional} of a {@code JsonNode} of the JSON from the
     * latest release of the repository, or an empty {@code Optional} if the JSON
     * could not be fetched
     */
    public static Optional<JsonNode> getJsonForLatestGithubRelease(String otbRepo) {
        if (tooFewRequestsRemaining()) {
            return Optional.empty();
        }

        HttpClient httpClient = HttpClientBuilder.create().build();
        String url = "https://api.github.com/repos/otbproject/" + otbRepo + "/releases/latest";
        HttpGet httpGet = new HttpGet(url);
        httpGet.addHeader("Accept", "application/vnd.github.v3+json");
        try {
            HttpResponse response = httpClient.execute(httpGet);

            // Get rate limiting info
            String infoString = response.toString();
            try {
                int xRateLimitRemaining = Integer.parseInt(infoString.split("X-RateLimit-Remaining: ")[1].split(",")[0]);
                long xRateLimitReset = Integer.parseInt(infoString.split("X-RateLimit-Reset: ")[1].split(",")[0]);
                JsonHandler.writeValue(RATE_LIMIT_INFO_PATH, new long[]{xRateLimitRemaining, xRateLimitReset});
            } catch (ArrayIndexOutOfBoundsException | NumberFormatException e) {
                App.logger.error("Error getting GitHub rate limit information");
                App.logger.catching(e);
            }

            return Optional.of(JsonHandler.MAPPER.readTree(new BasicResponseHandler().handleResponse(response)));
        } catch (IOException e) {
            App.logger.catching(Level.WARN, e);
            return Optional.empty();
        }
    }

    private static boolean tooFewRequestsRemaining() {
        Optional<Long[]> optional = JsonHandler.readValue(RATE_LIMIT_INFO_PATH, Long[].class);
        if (!optional.isPresent()) {
            return false;
        }
        Long[] info = optional.get();
        if (info.length != 2) {
            App.logger.warn("Unexpected array length when reading rate limiting information from file");
            return false;
        } else if ((info[0] == null) || info[1] == null) {
            App.logger.warn("Unexpected null values when reading rate limiting information from file");
            return false;
        }
        long remaining = info[0];
        long resetTime = info[1];

        return (remaining < 10) && (resetTime > Instant.now().getEpochSecond());
    }
}
import com.github.otbproject.otbproject.bot.Control;
import com.github.otbproject.otbproject.cli.ArgParser;
import com.github.otbproject.otbproject.cli.commands.CmdParser;
import com.github.otbproject.otbproject.config.*;
import com.github.otbproject.otbproject.fs.FSUtil;
import com.github.otbproject.otbproject.fs.PathBuilder;
import com.github.otbproject.otbproject.fs.Setup;
import com.github.otbproject.otbproject.fs.groups.Base;
import com.github.otbproject.otbproject.fs.groups.Chan;
import com.github.otbproject.otbproject.fs.groups.Load;
import com.github.otbproject.otbproject.gui.GuiApplication;
import com.github.otbproject.otbproject.messages.internal.InternalMessageSender;
import com.github.otbproject.otbproject.util.FatalChecker;
import com.github.otbproject.otbproject.util.LogRemover;
import com.github.otbproject.otbproject.util.ThreadUtil;
import com.github.otbproject.otbproject.util.Unpacker;
import com.github.otbproject.otbproject.util.compat.VersionCompatHelper;
import com.github.otbproject.otbproject.util.preload.LoadStrategy;
import com.github.otbproject.otbproject.util.version.AppVersion;
import com.github.otbproject.otbproject.util.version.Version;
import com.github.otbproject.otbproject.util.version.Versions;
import com.github.otbproject.otbproject.web.WebInterface;
import org.apache.commons.cli.CommandLine;
import org.apache.commons.cli.ParseException;
import org.apache.logging.log4j.Level;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import java.io.File;
import java.io.IOException;
import java.io.PrintStream;
import java.lang.management.ManagementFactory;
import java.text.DateFormat;
import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.Scanner;
package com.github.otbproject.otbproject;



public class App {
    public static final String PID = ManagementFactory.getRuntimeMXBean().getName().split("@")[0];
    public static final Logger logger = LogManager.getLogger();
    public static final Version VERSION = AppVersion.current();

    private App() {}

    public static void main(String[] args) {
        try {
            doMain(args);
        } catch (Throwable t) {
            try {
                System.err.println("A fatal problem has occurred.");
                t.printStackTrace();
                System.err.println("Attempting to log problem.");
                // log throwable
                DateFormat dateFormat = new SimpleDateFormat("yyyy.MM.dd-HH.mm.ss");
                Date date = new Date();
                File file = new File("OTBProjectFatal-" + dateFormat.format(date) + ".log");
                if (!file.createNewFile()) {
                    throw new IOException("Failed to create fatal log file for some reason.");
                }
                PrintStream ps = new PrintStream(file);
                t.printStackTrace(ps);
            } catch (IOException e) {
                e.printStackTrace();
            }
            throw t;
        }
    }


    private static void doMain(String[] args) {
        CommandLine cmd = initialArgParse(args);

        System.setProperty("OTBBASE", FSUtil.getBaseDir());