@FunctionalInterface
public interface CommandFilter
{
    void filter(CommandCall command, MessageContext context, ArgumentMap args);
} public static class PathSyntaxException extends RuntimeException
{
} public interface ArgumentFactory<T>
{
    T process(String argument) throws BadArgumentTypeException;
    T[] createArray();
} public class CommandAccessor
{
    private KrobotModule module;
    private KrobotCommand command;

    public CommandAccessor(KrobotModule module, KrobotCommand command)
    {
        this.module = module;
        this.command = command;
    }

    public CommandAccessor description(String desc)
    {
        command.setDescription(desc);
        return this;
    }

    public CommandAccessor alias(String... aliases)
    {
        command.setAliases(ArrayUtils.addAll(command.getAliases(), aliases));
        return this;
    }

    @SafeVarargs // If you don't want your whole command(...) chain to be underlined
    public final /* @SafeVarargs requires the method to be final */ CommandAccessor filter(Class<? extends CommandFilter>... filters)
    {
        return filter(Stream.of(filters)
                            .map(filter -> module.injector().getInstance(filter))
                            .toArray(CommandFilter[]::new));
    }

    public CommandAccessor filter(CommandFilter... filter)
    {
        command.getFilters().addAll(Arrays.asList(filter));
        return this;
    }

    public SubCommandAccessor sub(String path, Class<? extends CommandHandler> handler)
    {
        return sub(path, module.injector().getInstance(handler));
    }

    public SubCommandAccessor sub(String path, CommandHandler handler)
    {
        PathCompiler compiler = new PathCompiler(path);
        compiler.compile();

        KrobotCommand sub = new KrobotCommand(compiler.label(), compiler.args(), handler);
        this.command.getSubCommands().add(sub);

        return new SubCommandAccessor(module, this, sub);
    }

    public static class SubCommandAccessor extends CommandAccessor
    {
        private CommandAccessor parent;

        SubCommandAccessor(KrobotModule module, CommandAccessor parent, KrobotCommand child)
        {
            super(module, child);

            this.parent = parent;
        }

        @Override
        public SubCommandAccessor description(String desc)
        {
            return (SubCommandAccessor) super.description(desc);
        }

        @Override
        public SubCommandAccessor alias(String... aliases)
        {
            return (SubCommandAccessor) super.alias(aliases);
        }

        @SafeVarargs // If you don't want your whole command(...) chain to be underlined
        // @Override Couldn't override method, because @SafeVarargs makes it final. Renamed it to filterS
        public final /* @SafeVarargs requires the method to be final */ SubCommandAccessor filters(Class<? extends CommandFilter>... filters)
        {
            return (SubCommandAccessor) super.filter(filters);
        }

        @Override
        public SubCommandAccessor filter(CommandFilter... filter)
        {
            return (SubCommandAccessor) super.filter(filter);
        }

        public CommandAccessor then()
        {
            return this.parent;
        }
    }
} @FunctionalInterface
public interface CommandHandler
{
    Object handle(MessageContext context, ArgumentMap args) throws Exception;
} public class KrobotCommand
{
    private String label;
    private String[] aliases;
    private CommandArgument[] arguments;
    private String description;
    private boolean errorMP; //error in private message
    private boolean handleMP; //private message only
    private List<CommandFilter> filters;
    private List<KrobotCommand> subs;
    private CommandHandler handler;


    public KrobotCommand(String label, CommandArgument[] arguments, CommandHandler handler)
    {
        this(label, arguments, handler, new ArrayList<>());
    }

    public KrobotCommand(String label, CommandArgument[] arguments, CommandHandler handler, List<KrobotCommand> subs)
    {
        this(label, arguments, "", new String[] {}, false, false, new ArrayList<>(), handler, new ArrayList<>());
    }

    public KrobotCommand(String label, CommandArgument[] arguments, String description, String[] aliases, boolean errorMP, boolean handleMP, List<CommandFilter> filters, CommandHandler handler, List<KrobotCommand> subs)
    {
        this.label = label;
        this.aliases = aliases;
        this.arguments = arguments;
        this.description = description;
        this.errorMP = errorMP;
        this.handleMP = handleMP;
        this.filters = filters;
        this.subs = subs;
        this.handler = handler;
    }

    public String getLabel()
    {
        return label;
    }

    public void setLabel(String label)
    {
        this.label = label;
    }

    public String[] getAliases()
    {
        return aliases;
    }

    public void setAliases(String[] aliases)
    {
        this.aliases = aliases;
    }

    public CommandArgument[] getArguments()
    {
        return arguments;
    }

    public void setArguments(CommandArgument[] arguments)
    {
        this.arguments = arguments;
    }

    public String getDescription()
    {
        return description;
    }

    public void setDescription(String description)
    {
        this.description = description;
    }

    public boolean getErrorMP()
    {
    	return errorMP;
    }

    public void setErrorMP(boolean errorMP)
    {
    	this.errorMP = errorMP;
    }

    public boolean getHandleMP()
    {
    	return handleMP;
    }

    public void setHandleMP(boolean handleMP)
    {
    	this.handleMP = handleMP;
    }

    public List<CommandFilter> getFilters()
    {
        return filters;
    }

    public List<KrobotCommand> getSubCommands()
    {
        return subs;
    }

    public CommandHandler getHandler()
    {
        return handler;
    }

    public void setHandler(CommandHandler handler)
    {
        this.handler = handler;
    }

    @Override
    public String toString()
    {
        return toString("", true);
    }

    /**
     * Convert the command to a displayable string
     * (similar to the {@link PathCompiler} syntax)
     *
     * @param prefix The prefix to add (like tabs)
     * @param subs If the subs should be displayed too
     *
     * @return The generated string
     */
    public String toString(String prefix, boolean subs)
    {
        StringBuilder string = new StringBuilder(prefix + this.label + " ");

        for (CommandArgument argument : arguments)
        {
            string.append(argument).append(" ");
        }

        if (subs && this.subs != null && this.subs.size() != 0)
        {
            for (KrobotCommand sub : this.subs)
            {
                string.append("\n");
                string.append(prefix).append(sub.toString(prefix + this.getLabel() + " ", true));
            }
        }

        return string.toString();
    }
} public class ConfigAccessor
{
    private ConfigRules rules;

    public ConfigAccessor(ConfigRules rules)
    {
        this.rules = rules;
    }

    public ConfigAccessor named(String name)
    {
        this.rules.setName(name);
        return this;
    }

    public DefaultAccessor defaultIn()
    {
        return new DefaultAccessor(rules, this);
    }

    public static class DefaultAccessor
    {
        private ConfigRules rules;
        private ConfigAccessor configAccessor;

        DefaultAccessor(ConfigRules rules, ConfigAccessor configAccessor)
        {
            this.rules = rules;
            this.configAccessor = configAccessor;
        }

        public ConfigAccessor classpath(String path)
        {
            if (!path.startsWith("/"))
            {
                path = "/" + path;
            }

            this.rules.setDef(new ConfigRules.DefaultPath(path, ConfigRules.PathLocation.CLASSPATH));
            return this.configAccessor;
        }

        public ConfigAccessor file(String path)
        {
            this.rules.setDef(new ConfigRules.DefaultPath(path, ConfigRules.PathLocation.FILESYSTEM));
            return this.configAccessor;
        }
    }
} public class ConfigRules
{
    private String path;
    private String name;
    private DefaultPath def;

    public ConfigRules(String path)
    {
        this.path = path;
    }

    public String getPath()
    {
        return path;
    }

    public String getName()
    {
        return name;
    }

    public void setName(String name)
    {
        this.name = name;
    }

    public DefaultPath getDef()
    {
        return def;
    }

    public void setDef(DefaultPath def)
    {
        this.def = def;
    }

    public static class DefaultPath
    {
        private String path;
        private PathLocation location;

        DefaultPath(String path, PathLocation location)
        {
            this.path = path;
            this.location = location;
        }

        public String getPath()
        {
            return path;
        }

        public PathLocation getLocation()
        {
            return location;
        }

        @Override
        public String toString()
        {
            return "(from " + location.name().toLowerCase() + ") " + path;
        }
    }

    public enum PathLocation
    {
        CLASSPATH,
        FILESYSTEM
    }
} public class ConfigFolder
{
    private List<ConfigRules> configs;
    private File folder;

    public ConfigFolder(List<ConfigRules> configs, File folder)
    {
        this.configs = configs;
        this.folder = folder;
    }

    public ConfigAccessor config(String path)
    {
        ConfigRules rules = new ConfigRules(new File(folder, path).getPath());
        this.configs.add(rules);

        return new ConfigAccessor(rules);
    }

    public ConfigFolder configs(String... configs)
    {
        Stream.of(configs).forEach(this::config);
        return this;
    }

    public FolderDefaultAccessor withDefaultsIn()
    {
        return new FolderDefaultAccessor(this);
    }

    private List<ConfigRules> getConfigs()
    {
        return configs;
    }

    public static class FolderDefaultAccessor
    {
        private ConfigFolder configFolder;

        FolderDefaultAccessor(ConfigFolder configFolder)
        {
            this.configFolder = configFolder;
        }

        public ConfigFolder classpathFolder(String path)
        {
            if (!path.startsWith("/"))
            {
                path = "/" + path;
            }

            applyDef(path, ConfigRules.PathLocation.CLASSPATH);

            return this.configFolder;
        }

        public ConfigFolder folder(String path)
        {
            applyDef(path, ConfigRules.PathLocation.FILESYSTEM);
            return this.configFolder;
        }

        private void applyDef(String path, ConfigRules.PathLocation location)
        {
            configFolder.getConfigs().forEach(config -> {
                String name = Files.getNameWithoutExtension(config.getPath()) + ".default.json";
                config.setDef(new ConfigRules.DefaultPath(path + (path.endsWith("/") ? "" : "/") + name, location));
            });
        }
    }
} public abstract class ConsoleCommand
{
    public abstract String getPath();
    public abstract String getDescription();
    public abstract void execute(ArgumentMap args);
} @FunctionalInterface
public interface Filter
{
    boolean filter(MessageContext context);
} public class FilterAccessor
{
    private FilterRules rules;

    public FilterAccessor(FilterRules rules)
    {
        this.rules = rules;
    }

    public FilterAccessor prefix(String prefix)
    {
        rules.setPrefix(prefix);
        return this;
    }

    public FilterAccessor disable()
    {
        rules.setDisabled(true);
        return this;
    }

    public FilterAccessor apply(Handler handler)
    {
        rules.getHandlers().add(handler);
        return this;
    }
} public class FilterRules
{
    private Filter filter;

    private String prefix;
    private boolean disabled;
    private List<Handler> handlers;

    public FilterRules(Filter filter)
    {
        this.filter = filter;
        this.handlers = new ArrayList<>();
    }

    public String getPrefix()
    {
        return prefix;
    }

    public void setPrefix(String prefix)
    {
        this.prefix = prefix;
    }

    public boolean isDisabled()
    {
        return disabled;
    }

    public void setDisabled(boolean disabled)
    {
        this.disabled = disabled;
    }

    public List<Handler> getHandlers()
    {
        return handlers;
    }

    public Filter getFilter()
    {
        return filter;
    }
} public class ImportAccessor
{
    private ImportRules rules;

    public ImportAccessor(ImportRules rules)
    {
        this.rules = rules;
    }

    public ImportAccessor include(Includes... includes)
    {
        rules.setIncludes(includes);
        return this;
    }

    public ImportAccessor exclude(Includes... excludes)
    {
        for (Includes exclude : excludes)
        {
            rules.setIncludes(ArrayUtils.removeAllOccurences(rules.getIncludes(), exclude));
        }

        return this;
    }

    public ImportAccessor bridge(String sourceConfig, String targetConfig)
    {
        rules.getBridges().add(new ConfigBridge(sourceConfig, targetConfig));
        return this;
    }

    public ImportAccessor disableIf(Filter filter)
    {
        FilterRules rules = new FilterRules(filter);
        rules.setDisabled(true);

        this.rules.getFilters().add(rules);

        return this;
    }

    public ImportAccessor asSubsOf(String parentCommandLabel, String defaultCommand)
    {
        rules.setParentCommand(new ParentCommand(parentCommandLabel, defaultCommand));
        return this;
    }
} public class ImportRules
{
    private Class<? extends KrobotModule> module;

    private Includes[] includes;
    private List<ConfigBridge> bridges;
    private List<FilterRules> filters;
    private ParentCommand parentCommand;

    public ImportRules(Class<? extends KrobotModule> module)
    {
        this.module = module;

        this.includes = new Includes[] {COMMANDS, CONFIGS, EVENTS, FILTERS};
        this.bridges = new ArrayList<>();
        this.filters = new ArrayList<>();
    }

    public Includes[] getIncludes()
    {
        return includes;
    }

    public void setIncludes(Includes[] includes)
    {
        this.includes = includes;
    }

    public List<ConfigBridge> getBridges()
    {
        return bridges;
    }

    public List<FilterRules> getFilters()
    {
        return filters;
    }

    public ParentCommand getParentCommand()
    {
        return parentCommand;
    }

    public void setParentCommand(ParentCommand parentCommand)
    {
        this.parentCommand = parentCommand;
    }

    public Class<? extends KrobotModule> getModule()
    {
        return module;
    }

    public static class ConfigBridge
    {
        private String config;
        private String dest;

        ConfigBridge(String config, String dest)
        {
            this.config = config;
            this.dest = dest;
        }

        public String getConfig()
        {
            return config;
        }

        public String getDest()
        {
            return dest;
        }
    }

    public enum Includes
    {
        COMMANDS,
        CONFIGS,
        EVENTS,
        FILTERS
    }
} public class KrobotRuntime
{
    // TODO: Rolling logger

    public static final int DEFAULT_MAX_THREAD = 25;

    private static final ColoredLogger log = ColoredLogger.getLogger("Krobot");
    private static KrobotRuntime current;

    private long startTime;
    private long time;

    private Bot bot;
    private Class<? extends KrobotModule> botClass;
    private String token;

    private List<RuntimeModule> modules;
    private ComputedModule rootModule;

    private JDA jda;
    private Injector injector;
    private String prefix;
    private FilterRunner filterRunner;
    private CommandManager commandManager;

    private int maxThread;
    private ThreadPoolExecutor threadPool;

    private KrobotConsole console;
    private StateBar stateBar;
    private long lastExecutionTime;
    private long uptime;

    private KrobotRuntime(Class<? extends KrobotModule> botClass, String token)
    {
        this.botClass = botClass;
        this.token = token;

        this.modules = new ArrayList<>();

        this.maxThread = DEFAULT_MAX_THREAD;

        this.lastExecutionTime = 0;
    }

    private void launch()
    {
        startTime = System.currentTimeMillis();

        bot = botClass.getAnnotation(Bot.class);

        if (bot == null)
        {
            log.error("The provided bot class ({}) doesn't have the @Bot annotation", botClass.getName());
            System.exit(1);
        }

        String prop = System.getProperty(Krobot.PROPERTY_DISABLE_COLORS);

        if (System.console() == null && (prop == null || !prop.equalsIgnoreCase("false")))
        {
            System.err.println("\n/!\\ No console detected => Disabled colors /!\\\n");
            prop = "true";
        }

        if (prop == null || !prop.equalsIgnoreCase("true"))
        {
            AnsiConsole.systemInstall();
        }
        else
        {
            Ansi.setEnabled(false);
        }

        String disableProperty = System.getProperty(Krobot.PROPERTY_DISABLE_START_MESSAGE);

        if (disableProperty == null || !disableProperty.equalsIgnoreCase("true"))
        {
            log.info(ansi().eraseScreen());
            log.info("                                     ");
            log.infoBold(BLUE, "         hMMMMM:      -dMMMMMMs`     ");
            log.infoBold(BLUE, "        +MMMMMs     oNMMMMMd-        ");
            log.infoBold(BLUE, "       .MMMMMm   -dMMMMMN+           ");
            log.infoBold(BLUE, "       +MMMMMo.yMMMMMNo`             ");
            log.infoBold(BLUE, "       dMMMMMyMMMMMMy`               ");
            log.infoBold(BLUE, "      oMMMMMMMMMMMMMMy               ");
            log.infoBold(BLUE, "      mMMMMMMNs-mMMMMMo              ");
            log.infoBold(BLUE, "     -MMMMMNo`  .NMMMMM+             ");
            log.infoBold(BLUE, "    sMMMMMd       yMMMMMN.           ");
            log.infoBold(BLUE, "   sMMMMM+        `dMMMMMd`          ");
            log.infoBold(BLUE, "   /+++++`         `++++++-          ");
            log.infoBold(BLUE, "                                     \n");
        }

        log.infoBold("--> Starting {} v{} by {}\n", bot.name(), bot.version(), bot.author());

        log.info("Running Krobot 3.0.0");
        log.info("Copyright (c) 2017 The Krobot Contributors\n");

        log.infoBold("----> 1/4 Pre-initialization");
        timerStart();

        log.info("Computing modules...");

        ModuleLoader loader = new ModuleLoader();
        rootModule = loader.load(botClass);

        List<ComputedModule> modules = loader.getModules();

        modules.stream()
                .map(ComputedModule::getModule)
                .forEach(module -> Stream.of(module.getClass().getDeclaredFields())
                .filter(field -> field.isAnnotationPresent(LoadModule.class))
                .forEach(field -> {
                    field.setAccessible(true); try
                    {
                        field.set(module,
                                modules.stream()
                                        .map(ComputedModule::getModule)
                                        .filter(m -> m.getClass() == field.getType())
                                        .findFirst()
                                        .orElseThrow(() -> new RuntimeException("Cannot load module " + field.getType().getName() + " as it isn't imported. @LoadModule annotation can't be used without @Include / from(...)")));
                    }
                    catch (IllegalAccessException ignored) {}
                    field.setAccessible(false);
                }));

        log.info("Processing configs...");

        modules.forEach(source -> {
            RuntimeModule module = new RuntimeModule(source);

            ConfigLoader configLoader = new ConfigLoader(module);
            configLoader.load();

            this.modules.add(module);
        });

        log.infoBold("----> Done in " + timerGet() + "ms\n");

        log.infoBold("----> 2/4 Initialization");
        timerStart();

        log.info("Processing dependency injection...");

        List<Module> guiceModules = new ArrayList<>();
        modules.stream().map(m -> m.getModule().getGuiceModules()).forEach(guiceModules::addAll);
        guiceModules.add(new KrobotGuiceModule(this));

        injector = Guice.createInjector(guiceModules);
        this.modules.forEach((module) ->
        {
            KrobotModule m = module.getComputed().getModule();

            try
            {
                Field field = KrobotModule.class.getDeclaredField("injector");

                field.setAccessible(true);
                {
                    field.set(m, injector.createChildInjector(new ModuleModule(module)));
                }
                field.setAccessible(false);
            }
            catch (IllegalAccessException | NoSuchFieldException e)
            {
                e.printStackTrace();
            }
        });

        modules.stream().map(ComputedModule::getModule).forEach(module ->
        {
            log.info("({}) Initialization...", module.getClass().getName());

            module.injector().injectMembers(module);
            module.init();
        });

        prefix = rootModule.getModule().getPrefix();

        log.info("Processing filters...");

        modules.forEach(module -> module.getFilters().addAll(module.getModule().getFilters()));

        filterRunner = new FilterRunner(this, modules.toArray(new ComputedModule[modules.size()]));

        modules.forEach(module -> module.getModule().getCommands().forEach(command ->
        {
            command.getFilters().add((call, context, args) -> {
                if (filterRunner.isDisabled(context, module.getModule()))
                {
                    call.setCancelled(true);
                }
            });
        }));

        log.info("Processing commands...");

        commandManager = injector.getInstance(CommandManager.class);

        modules.forEach(module -> module.getModule().getCommandFilters().forEach(filter ->
        {
            commandManager.getFilters().add((command, context, args) -> {
                if (!filterRunner.isDisabled(context, module.getModule()))
                {
                    filter.filter(command, context, args);
                }
            });
        }));

        modules.forEach(module -> {
            Class<? extends KrobotModule> cl = module.getModule().getClass();

            if (cl.isAnnotationPresent(Include.class))
            {
                Stream.of(cl.getAnnotation(Include.class).filters()).map(injector::getInstance).forEach(commandManager.getFilters()::add);
            }
        });
        
        modules.forEach(module -> {
            Class<? extends KrobotModule> cl = module.getModule().getClass();

            if (cl.isAnnotationPresent(Include.class))
            {
                Class<? extends CommandHandler>[] classes = cl.getAnnotation(Include.class).commands();

                for (Class<? extends CommandHandler> commandClass : classes)
                {
                    KrobotCommand command = registerCommandClass(module.getModule(), commandClass);

                    if (command != null)
                    {
                        KrobotCommand parent = module.getParentCommand();

                        if (parent != null)
                        {
                            parent.getSubCommands().add(command);
                            continue;
                        }

                        addCommand(module.getModule(), command);
                    }
                }
            }
        });

        modules.forEach(m -> {
            KrobotCommand parent = m.getParentCommand();
            List<KrobotCommand> commands = m.getModule().getCommands();

            if (parent != null)
            {
                parent.getSubCommands().addAll(Arrays.asList(commands.toArray(new KrobotCommand[0])));
                addCommand(m.getModule(), parent);

                return;
            }

            commands.forEach(c -> addCommand(m.getModule(), c));
        });

        List<String> labels = new ArrayList<>();
        commandManager.getCommands().forEach(command -> {
            if (labels.contains(command.getLabel()))
            {
                log.error(Color.RED, "Duplicated command '{}' => One will be randomly executed", command.getLabel());
            }

            labels.add(command.getLabel());
        });

        log.info("Registered {} commands", commandManager.getCommands().size());

        console = new KrobotConsole(this);
        console.register(new ExitCommand());
        console.register(new HelpCommand(console));

        modules.forEach(m -> m.getModule().getConsoleCommands().forEach(c -> console.register(c)));

        modules.stream().map(ComputedModule::getModule).filter(m -> m.getClass().isAnnotationPresent(Include.class)).forEach(m -> {
            Stream.of(m.getClass().getAnnotation(Include.class).consoleCommands()).forEach(console::register);
        });

        log.info("Registered {} console commands", console.getCommands().size());

        log.infoBold("----> Done in " + timerGet() + "ms\n");

        log.infoBold("----> 3/4 Starting JDA");
        timerStart();

        try
        {
            jda = JDABuilder.createDefault(token)
                .setEventManager(new AnnotatedEventManager())
                .addEventListeners(this)
                .setToken(token)
                .build()
                .awaitReady();
        }
        catch (LoginException e)
        {
            if (e.getCause() instanceof UnknownHostException)
            {
                log.error("You aren't connected to the Internet ! Exiting...");
            }
            else
            {
                log.error("Wrong bot token provided ! Exiting...");
            }

            System.exit(1);
        }
        catch (InterruptedException ignored)
        {
        }

        modules.forEach(m -> {
            m.getModule().getEventListeners().forEach(jda::addEventListener);

            Class<? extends KrobotModule> moduleClass = m.getModule().getClass();

            if (moduleClass.isAnnotationPresent(Include.class))
            {
                Object[] listeners = Stream.of(moduleClass.getAnnotation(Include.class).listeners()).map(m.getModule().injector()::getInstance).toArray();
                jda.addEventListener(listeners);
            }
        });

        log.infoBold("----> Done in " + timerGet() + "ms\n");

        log.infoBold("----> 4/4 Post-Initialization");
        timerStart();

        modules.stream().map(ComputedModule::getModule).forEach(module ->
        {
            log.info("({}) Post-Initialization...", module.getClass().getName());

            module.postInit();
        });

        if (System.console() == null)
        {
            if (System.getProperty(Krobot.PROPERTY_DISABLE_STATE_BAR) == null)
            {
                log.warn(Color.YELLOW, "No console detected, you are probably running from an IDE");
                log.warn(Color.YELLOW, "In these case, state bar can be really buggy");
                log.warn(Color.YELLOW, "If they are, disable them by adding -Dkrobot.disableStateBar=true in the run VM args\n");
            }

            // Disabling ugly JLine message
            Logger.getLogger("org.jline").setLevel(Level.OFF);
        }

        this.threadPool = (ThreadPoolExecutor) Executors.newFixedThreadPool(maxThread);

        log.infoBold("----> Done in " + timerGet() + "ms\n");

        log.infoAuto("@|green Now running|@ @|bold,green {} v{} by {}|@ @|green [{}] (started in|@ @|bold,green {}ms|@@|green )|@", bot.name(), bot.version(), bot.author(), botClass.getName(), System.currentTimeMillis() - startTime);
        log.infoAuto("@|green Press any key to enter a command, do '|@@|bold,green exit|@@|green ' to close|@\n");

        String disable = System.getProperty(Krobot.PROPERTY_DISABLE_STATE_BAR);
        if (disable == null || !disable.equals("true"))
        {
            stateBar = new StateBar(this);
            stateBar.start();
        }

        disable = System.getProperty(Krobot.PROPERTY_DISABLE_CONSOLE);
        if (disable == null || !disable.equals("true"))
        {
            console.start();
        }

        uptime = System.currentTimeMillis();
    }

    protected void addCommand(KrobotModule source, KrobotCommand command)
    {
        command.getFilters().add((call, context, args) -> {
            if (filterRunner.isDisabled(context, source))
            {
                call.setCancelled(true);
            }
        });

        commandManager.getCommands().add(command);
    }

    protected KrobotCommand registerCommandClass(KrobotModule module, Class<? extends CommandHandler> commandClass)
    {
        if (!commandClass.isAnnotationPresent(Command.class))
        {
            log.error(Color.RED, "Class '{}' declared in @Include#commands is missing @Command annotation", commandClass.getName());
            log.error(Color.RED, "Command will not be registered");

            return null;
        }

        Command command = commandClass.getAnnotation(Command.class);

        PathCompiler compiler = new PathCompiler(command.value());

        try
        {
            compiler.compile();
        }
        catch (PathCompiler.PathSyntaxException e)
        {
            System.exit(1);
        }

        Injector injector = module.injector();

        List<CommandFilter> filters = Stream.of(command.filters()).map(injector::getInstance).collect(Collectors.toList());
        List<KrobotCommand> subs = Stream.of(command.subs()).map(c -> registerCommandClass(module, c)).collect(Collectors.toList());

        return new KrobotCommand(compiler.label(), compiler.args(), command.desc(), command.aliases(), command.errorMP(), command.handleMP(), filters, injector.getInstance(commandClass), subs);
    }

    @SubscribeEvent
    public void onMessage(MessageReceivedEvent event)
    {
        if (event.getAuthor() instanceof SelfUser) return;

        MessageChannel channel;
        if (event.isFromType(ChannelType.TEXT)) channel = event.getChannel();
        else if (event.isFromType(ChannelType.PRIVATE)) channel = event.getAuthor().openPrivateChannel().complete();
        else return;

        final MessageContext context = new MessageContext(event.getJDA(), event.getAuthor(), event.getMessage(), channel);
        handle(context);
    }

    protected void handle(MessageContext context)
    {
        if (threadPool == null)
        {
            return;
        }

        threadPool.submit(() -> {
            long time = System.currentTimeMillis();

            try
            {
                filterRunner.runFilters(context);
                commandManager.handle(context);
            }
            catch (Exception e)
            {
                log.error("Error while handling message", e);
            }

            setLastExecutionTime(System.currentTimeMillis() - time);
        });
    }

    private void end()
    {
        log.infoAuto("@|bold ----> Shutting down...|@");

        if (stateBar != null)
        {
            stateBar.interrupt();
        }

        jda.shutdown();
    }

    public ExceptionHandler getExceptionHandler()
    {
        return injector.getInstance(ExceptionHandler.class);
    }

    public RuntimeModule getRuntimeModule(Class<? extends KrobotModule> moduleClass)
    {
        for (RuntimeModule module : modules)
        {
            if (module.getComputed().getModule().getClass() == moduleClass)
            {
                return module;
            }
        }

        return null;
    }

    public boolean isRootModule(KrobotModule module)
    {
        return module == getRootModule().getModule();
    }

    public Injector getInjector()
    {
        return injector;
    }

    public ComputedModule getRootModule()
    {
        return rootModule;
    }

    public Bot getBot()
    {
        return bot;
    }

    public ThreadPoolExecutor getThreadPool()
    {
        return threadPool;
    }

    public FilterRunner getFilterRunner()
    {
        return filterRunner;
    }

    public JDA jda()
    {
        return jda;
    }

    public String getPrefix()
    {
        return prefix;
    }

    public void setMaxThread(int maxThread)
    {
        this.maxThread = maxThread;
    }

    public long getLastExecutionTime()
    {
        return lastExecutionTime;
    }

    public long getUptime()
    {
        return System.currentTimeMillis() - uptime;
    }

    public CommandManager getCommandManager()
    {
        return commandManager;
    }

    public StateBar getStateBar()
    {
        return stateBar;
    }

    public KrobotConsole getConsole()
    {
        return console;
    }

    private synchronized void setLastExecutionTime(long lastExecutionTime)
    {
        this.lastExecutionTime = lastExecutionTime;
    }

    private void timerStart()
    {
        time = System.currentTimeMillis();
    }

    private long timerGet()
    {
        return System.currentTimeMillis() - time;
    }

    public static KrobotRuntime start(Class<? extends KrobotModule> bot, String key)
    {
        if (current != null)
        {
            log.warn("Another runtime is tried to be launched when one is currently running");
            log.warn("Bot : " + current.botClass.getName());
            log.warn("Stopping the old one... Next time consider calling KrobotRuntime.stop()");

            stop();
        }

        current = new KrobotRuntime(bot, key);
        current.launch();

        return current;
    }

    public static boolean isStarted()
    {
        return current != null;
    }

    public static KrobotRuntime get()
    {
        if (current == null)
        {
            log.warn("KrobotRuntime.get() was called without any runtime launched, this will probably lead to a NullPointerException");
            log.warn("Consider using KrobotRuntime.isStarted() to check if a bot was launched");
        }

        return current;
    }

    public static void stop()
    {
        if (current == null)
        {
            log.error("Tried to stop the runtime, but no one was started, nothing will be done");
            return;
        }

        get().end();
    }

    public List<RuntimeModule> getModules()
    {
        return modules;
    }
} public class PathCompiler
{
    private static final ColoredLogger log = ColoredLogger.getLogger("PathCompiler");

    private String path;

    private int cursor;

    private String label;
    private List<CommandArgument> args;

    public PathCompiler(String path)
    {
        this.path = path;

        this.cursor = 0;
        this.args = new ArrayList<>();
    }

    public void compile()
    {
        label = readWord();

        while (true)
        {
            if (!hasNext())
            {
                break;
            }

            skipEnsure(' ');

            switch (next("'[' or '<'"))
            {
                case '<':
                    readArg(true);
                    break;
                case '[':
                    readArg(false);
                    break;
                default:
                    throw error("Expected '<' or '[' got '" + previous() + "'");
            }
        }
    }

    protected void readArg(boolean required)
    {
        int start = cursor;
        char end = required ? '>' : ']';

        String name = readWord();

        if (args.stream().anyMatch(arg -> arg.getKey().equalsIgnoreCase(name)))
        {
            throw error("Duplicated argument '" + name + "'", cursor - name.length() + 1, cursor);
        }

        String type = null;

        char next = next("':' or '" + end + "'");

        if (next == ':')
        {
            type = readWord();
            next = next("'...', '|' or '" + end + "'");
        }

        ArgumentFactory factory = null;
        boolean list = false;

        if (type != null && next == '|')
        {
            List<String> choices = new ArrayList<>();
            choices.add(type);

            while (next == '|')
            {
                choices.add(readWord());
                next = next("'|' or '" + end + "'");
            }

            type = String.join("|", choices);
            String finalType = type;

            factory = new ArgumentFactory<String>()
            {
                @Override
                public String process(String argument) throws BadArgumentTypeException
                {
                    if (!choices.contains(argument))
                    {
                        throw new BadArgumentTypeException("Can only be one of : " + String.join(", ", choices) + "; but not '" + argument + "'", argument, finalType);
                    }

                    return argument;
                }

                @Override
                public String[] createArray()
                {
                    return new String[0];
                }
            };
        }
        else
        {
            if (next == '.')
            {
                skipEnsure('.');
                skipEnsure('.');

                list = true;

                skipEnsure(end);
            }
            else if (next != end)
            {
                throw error("Expected " + (type == null ? "':' or " : "") + "'" + end + "' got '" + next + "'");
            }
        }

        if (args.size() > 0)
        {
            CommandArgument last = args.get(args.size() - 1);

            if (required && !last.isRequired())
            {
                throw error("Can't put a required argument after an optional one", start, cursor);
            }
            else if (last.isList())
            {
                throw error("List argument must stay the last one", start, cursor);
            }
        }

        type = type == null ? "string" : type;
        factory = factory == null ? CommandManager.getArgumentFactory(type) : factory;

        if (factory == null)
        {
            throw error("Unknown argument type '" + type + "'", cursor - type.length(), cursor - 1);
        }

        args.add(new CommandArgument(required, name, type, factory, list));
    }

    protected void skipEnsure(char c)
    {
        char next = next("'" + c + "'");

        if (next != c)
        {
            throw error("Expected '" + c + "' got '" + next + "'");
        }
    }

    protected String readWord()
    {
        StringBuilder result = new StringBuilder();
        char current;

        while (cursor < path.length())
        {
            current = current();

            if (!Character.isLetterOrDigit(current) && current != '-')
            {
                break;
            }

            result.append(current);
            cursor++;
        }

        return result.toString();
    }

    protected boolean hasNext()
    {
        return cursor < path.length();
    }

    protected char previous()
    {
        return path.charAt(cursor - 1);
    }

    protected char current()
    {
        return path.charAt(cursor);
    }

    protected char next(String expectation)
    {
        if (cursor >= path.length())
        {
            throw error("Unexpected end of path, expected '" + expectation + "'");
        }

        char val = current();
        cursor++;

        return val;
    }

    protected PathSyntaxException error(String message)
    {
        return error(message, cursor);
    }

    protected PathSyntaxException error(String message, int to)
    {
        return error(message, cursor, to);
    }

    protected PathSyntaxException error(String message, int from, int to)
    {
        log.error("");
        log.errorAuto("@|bold,red Error:|@ @|bold,white {}|@", message);
        log.errorAuto("    @|bold,blue -->|@ {}", path);
        log.errorAuto("        " + repeat(' ', from - 1) + "@|bold,red ^" + repeat('^', to - from) + "|@");
        log.errorBold(Color.WHITE, "Aborting due to command path compilation error");

        return new PathSyntaxException();
    }

    protected String repeat(char c, int amount)
    {
        StringBuilder result = new StringBuilder();

        for (int i = 0; i < amount; i++)
        {
            result.append(c);
        }

        return result.toString();
    }

    public String label()
    {
        return this.label;
    }

    public CommandArgument[] args()
    {
        return this.args.toArray(new CommandArgument[this.args.size()]);
    }

    public String getPath()
    {
        return path;
    }

    public static class PathSyntaxException extends RuntimeException
    {
    }
} public class ImportRules
{
    private Class<? extends KrobotModule> module;

    private Includes[] includes;
    private List<ConfigBridge> bridges;
    private List<FilterRules> filters;
    private ParentCommand parentCommand;

    public ImportRules(Class<? extends KrobotModule> module)
    {
        this.module = module;

        this.includes = new Includes[] {COMMANDS, CONFIGS, EVENTS, FILTERS};
        this.bridges = new ArrayList<>();
        this.filters = new ArrayList<>();
    }

    public Includes[] getIncludes()
    {
        return includes;
    }

    public void setIncludes(Includes[] includes)
    {
        this.includes = includes;
    }

    public List<ConfigBridge> getBridges()
    {
        return bridges;
    }

    public List<FilterRules> getFilters()
    {
        return filters;
    }

    public ParentCommand getParentCommand()
    {
        return parentCommand;
    }

    public void setParentCommand(ParentCommand parentCommand)
    {
        this.parentCommand = parentCommand;
    }

    public Class<? extends KrobotModule> getModule()
    {
        return module;
    }

    public static class ConfigBridge
    {
        private String config;
        private String dest;

        ConfigBridge(String config, String dest)
        {
            this.config = config;
            this.dest = dest;
        }

        public String getConfig()
        {
            return config;
        }

        public String getDest()
        {
            return dest;
        }
    }

    public enum Includes
    {
        COMMANDS,
        CONFIGS,
        EVENTS,
        FILTERS
    }
}
import com.google.inject.Injector;
import com.google.inject.Module;
import java.io.File;
import java.util.Arrays;
import java.util.HashMap;
import java.util.Map;
import java.util.function.Function;
import java.util.stream.Stream;
import net.dv8tion.jda.api.JDA;
import net.dv8tion.jda.api.entities.Guild;
import org.krobot.command.CommandFilter;
import org.krobot.command.PathCompiler.PathSyntaxException;
import org.krobot.command.ArgumentFactory;
import org.krobot.command.CommandAccessor;
import org.krobot.command.CommandHandler;
import org.krobot.command.KrobotCommand;
import org.krobot.config.ConfigAccessor;
import org.krobot.config.ConfigRules;
import org.krobot.config.ConfigFolder;
import org.krobot.console.ConsoleCommand;
import org.krobot.module.Filter;
import org.krobot.module.FilterAccessor;
import org.krobot.module.FilterRules;
import org.krobot.module.ImportAccessor;
import org.krobot.module.ImportRules;
import java.util.ArrayList;
import java.util.List;
import org.krobot.runtime.KrobotRuntime;
import org.krobot.command.PathCompiler;
import static org.krobot.module.ImportRules.*;
/*
 * Copyright 2017 The Krobot Contributors
 *
 * This file is part of Krobot.
 *
 * Krobot is free software: you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation, either version 3 of the License, or
 * (at your option) any later version.
 *
 * Krobot is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with Krobot.  If not, see <http://www.gnu.org/licenses/>.
 */
package org.krobot;





public abstract class KrobotModule
{
    protected static final Includes COMMANDS = Includes.COMMANDS;
    protected static final Includes CONFIGS = Includes.CONFIGS;
    protected static final Includes EVENTS = Includes.EVENTS;
    protected static final Includes FILTERS = Includes.FILTERS;

    protected Injector injector;

    private List<ImportRules> imports;
    private List<ConfigRules> configs;
    private List<FilterRules> filters;
    private List<Object> eventListeners;

    private String prefix;
    private List<KrobotCommand> commands;
    private List<CommandFilter> commandFilters;
    private Map<String, ArgumentFactory> argTypes;

    private List<Module> guiceModules;

    private List<ConsoleCommand> consoleCommands;

    public KrobotModule()
    {
        this.imports = new ArrayList<>();
        this.configs = new ArrayList<>();
        this.filters = new ArrayList<>();
        this.eventListeners = new ArrayList<>();

        this.commands = new ArrayList<>();
        this.commandFilters = new ArrayList<>();
        this.argTypes = new HashMap<>();

        this.guiceModules = new ArrayList<>();

        this.consoleCommands = new ArrayList<>();
    }

    public abstract void preInit();
    public abstract void init();
    public abstract void postInit();

    protected ImportAccessor from(Class<? extends KrobotModule> module)
    {
        ImportRules rules = new ImportRules(module);
        this.imports.add(rules);

        return new ImportAccessor(rules);
    }

    protected ConfigAccessor config(String path)
    {
        ConfigRules rules = new ConfigRules(path);
        this.configs.add(rules);

        return new ConfigAccessor(rules);
    }

    protected ConfigFolder folder(String path)
    {
        return folder(new File(path));
    }

    protected ConfigFolder folder(File file)
    {
        return new ConfigFolder(configs, file);
    }

    protected FilterAccessor when(Filter filter)