public class CommandRegistry {
    private static final Map<String, Command> commands = new HashMap<>();
    public static final Map<String, ServerConfig> serverConfigs = new HashMap<>();

    private static final SimpleLog pmLog = SimpleLog.getLog("PM");
    private static final SimpleLog mentionLog = SimpleLog.getLog("Mention");
    private static final SimpleLog commandLog = SimpleLog.getLog("Command");

    private static long msgCount = 0;
    private static int cmdCount = 0;

    public static void loadCommands(JDA api) {
        commands.clear();
        ClassEnumerator.getClassesForPackage(CommandSection.class.getPackage()).stream()
                .filter(aClass -> CommandSection.class.isAssignableFrom(aClass) && !aClass.equals(CommandSection.class))
                .forEach(aClass -> {
            @SuppressWarnings("unchecked")
            Class<? extends CommandSection> sectionClass = (Class<? extends CommandSection>) aClass;
            try {
                CommandSection section = sectionClass.newInstance();
                section.register(commands, api);
            } catch(InstantiationException | IllegalAccessException e) {
                e.printStackTrace();
            }
        });
    }

    public static void handle(MessageEvent event) {
        msgCount++;

        //load correct config-file
        ServerConfig cfg;
        if(!event.isPrivate()) {
            cfg = serverConfigs.get(event.getGuild().getId());
            if(event.getMessage().getMentionedUsers().contains(event.getJDA().getSelfInfo()) || event.getMessage().getMentionedUsers().contains(Statics.botOwner)) {
                mentionLog.info(String.format("[%s][%s] %s:%s", event.getGuild().getName(), event.getTextChannel().getName(),
                        event.getAuthor().getUsername(), event.getContent()));
            }
        } else {
            cfg = ServerConfig.PMConfig.getInstance(event.getJDA());
            if(event.getAuthor() != event.getJDA().getSelfInfo())
                pmLog.info(event.getAuthor().getUsername() + ": " + event.getContent());
        }


        //let modules handle the message and break if requested
        if(cfg.getModules().values().stream().map(m -> m.handle(event, cfg)).anyMatch(b -> b)) {
            return;
        }

        //stop if message is not created or user is blacklisted
        if(event.isEdit() || BlackList.contains(event.getAuthor())) {
            return;
        }

        //handle hard-coded commands
        if(event.getMessage().getContent().equals("-kbreset") && cfg.isOwner(event.getAuthor())) {
            cfg.setPrefix(ServerConfig.DEFAULT_PREFIX);
            reply(event, cfg, "Prefix was reset to default (" + ServerConfig.DEFAULT_PREFIX + ")");
            return;
        }
        if(event.getMessage().getContent().equals("-kbprefix")) {
            reply(event, cfg, "Current command-prefix is: `" + cfg.getPrefix() + '`');
            return;
        }
        if(event.isPrivate() && event.getContent().equalsIgnoreCase("help")) {
            commands.get("help").accept(event, cfg);
            return;
        }

        //Handle registered commands
        if(event.getContent().startsWith(cfg.getPrefix())) {
            String[] args = MessageUtil.getArgs(event, cfg);
            if(commands.containsKey(args[0])) {
                if(commands.get(args[0]).isAvailable(event, cfg)) {
                    cmdCount++;
                    commandLog.info(String.format("[%s][%s] %s: %s", event.isPrivate() ? "PM" : event.getGuild().getName(),
                            event.isPrivate() ? event.getAuthor().getUsername() : event.getTextChannel().getName(),
                            event.getAuthor().getUsername(), event.getMessage().getContent().substring(cfg.getPrefix().length())));
                    commands.get(args[0]).accept(event, cfg);
                }

            } else if(cfg.getCommands().containsKey(args[0])) {
                if(cfg.getCommands().get(args[0]).isAvailable(event, cfg)) {
                    cmdCount++;
                    commandLog.info(String.format("[%s][%s] %s: %s", event.isPrivate() ? "PM" : event.getGuild().getName(),
                            event.isPrivate() ? event.getAuthor().getUsername() : event.getTextChannel().getName(),
                            event.getAuthor().getUsername(), event.getMessage().getContent().substring(cfg.getPrefix().length())));
                    cfg.getCommands().get(args[0]).accept(event, cfg);
                }
            } else if(!event.isPrivate()) {
                if(cfg.isRestrictTexts() && !cfg.isMod(event.getAuthor())) {
                    //texts only available to mods
                    return;
                }
                Map<String, String> textCommands = cfg.getTextCommands();
                if(textCommands.containsKey(args[0])) {
                    reply(event, cfg, textCommands.get(args[0]));
                }
            }
        }
    }

    public static long getMessageCount() {
        return msgCount;
    }

    public static int getCommandCount() {
        return cmdCount;
    }
} public class BotConfig {
    private static final Path configPath = Paths.get("kanzeBotConfig.json");
    private static JSONObject config;

    public static void set(@NotNull String key, @Nullable Object val) {
        if(val == null) {
            config.remove(key);
        } else {
            config.put(key, val);
        }
        save();
    }

    @SuppressWarnings("unchecked")
    public static <T> T get(@NotNull String key) {
        if(config.has(key)) {
            try {
                return (T) config.get(key);
            } catch(JSONException | ClassCastException ignored) {}
        }
        return null;
    }

    public static <T> T get(@NotNull String key, @NotNull T def) {
        T obj = get(key);
        if(obj == null) {
            obj = def;
            config.put(key, def);
            save();
        }
        return obj;
    }

    public static void save() {
        try {
            Files.write(configPath, config.toString(4).getBytes(StandardCharsets.UTF_8));
        } catch(IOException ignored) {
        }
    }

    public static boolean load() {
        boolean exists = Files.exists(configPath);
        try {
            JSONObject def = getDefault();
            if(!exists) {
                Files.write(configPath, def.toString(4).getBytes(StandardCharsets.UTF_8), StandardOpenOption.CREATE);
            } else {
                config = new JSONObject(new String(Files.readAllBytes(configPath), StandardCharsets.UTF_8));
                for(String key : def.keySet()) {
                    if(!config.has(key)) {
                        config.put(key, def.get(key));
                        exists = false;
                    }
                }
                if(!exists) {
                    save();
                }
            }
        } catch(IOException ex) {
            System.err.println("Error reading/writing config file: ");
            ex.printStackTrace();
            return false;
        }
        return exists;
    }

    private static JSONObject getDefault() {
        return new JSONObject()
                .put("ownerId", "")
                .put("logname", "KanzeBot")
                .put("carbonKey", "")
                .put("oauthAppId", "")
                .put("logToFiles", true)
                .put("db", new JSONObject()
                        .put("host", "")
                        .put("database", "")
                        .put("user", "")
                        .put("password", "")
                ).put("historyBase", "");       //eg http://vps.kantenkugel.com:3000/history/
    }

    private BotConfig() {
    }
} public class DbListener implements EventListener {

    @Override
    public void onEvent(Event event) {
        if(event.getClass() == ReadyEvent.class) {                          //Init
            DbEngine.handleGuilds(event.getJDA().getGuilds());
        } else if(event.getClass() == ReconnectedEvent.class) {             //Reinit
            DbEngine.handleGuilds(event.getJDA().getGuilds());
        } else if(event.getClass() == UserNameUpdateEvent.class) {          //username
            DbEngine.updateUser(((UserNameUpdateEvent) event).getUser());
        } else if(event.getClass() == GuildJoinEvent.class) {               //Guild Join
            DbEngine.updateGuild(((GuildJoinEvent) event).getGuild());
        } else if(event.getClass() == GuildLeaveEvent.class) {              //Guild Leave
            DbEngine.deleteGuild(((GuildLeaveEvent) event).getGuild());
        } else if(event.getClass() == GuildUpdateEvent.class) {             //Guild Update
            DbEngine.updateGuild(((GuildUpdateEvent) event).getGuild());
        } else if(event.getClass() == TextChannelCreateEvent.class) {       //TextChannel Create
            DbEngine.updateChannel(((TextChannelCreateEvent) event).getChannel());
        } else if(event.getClass() == TextChannelDeleteEvent.class) {       //TextChannel Delete
            DbEngine.deleteChannel(((TextChannelDeleteEvent) event).getChannel());
        } else if(event.getClass() == TextChannelUpdateNameEvent.class) {   //TextChannel Name-update
            DbEngine.updateChannel(((TextChannelUpdateNameEvent) event).getChannel());
        } else if(event.getClass() == MessageReceivedEvent.class) {         //Message Received
            MessageEvent e = new MessageEvent(((MessageReceivedEvent) event).getMessage(), event.getResponseNumber());

            if(!e.isPrivate() &&  e.getContent().startsWith("-kbhistory")
                    && (MessageUtil.isGlobalAdmin(e.getAuthor()) || e.getGuild().getOwner() == e.getAuthor())) {
                MessageUtil.reply(e, new MessageBuilder().appendString("History-link: ")
                        .appendString(BotConfig.get("historyBase"))
                        .appendString(Long.toString(
                                DbEngine.createHistory(
                                        e.getMessage().getMentionedUsers().size() == 0 ? e.getAuthor() : e.getMessage().getMentionedUsers().get(0)
                                        , e.getTextChannel()))).build());
            } else if(MessageUtil.isGlobalAdmin(e.getAuthor()) && e.getContent().equals("-kbshutdown")) {
                MiscUtil.shutdown(Statics.NORMAL_EXIT_CODE);
            }

            DbEngine.handleMessage(e);
        } else if(event.getClass() == MessageUpdateEvent.class) {           //Message Updated
            MessageEvent e = new MessageEvent(((MessageUpdateEvent) event).getMessage(), event.getResponseNumber());
            DbEngine.handleMessage(e);
        } else if(event.getClass() == MessageDeleteEvent.class) {           //Message Deleted
            MessageDeleteEvent e = (MessageDeleteEvent) event;
            DbEngine.deleteMessage(e.getMessageId());
        }
    }
} public class InviteListener implements EventListener {
    @Override
    public void onEvent(Event e) {
        if(e.getClass() == InviteReceivedEvent.class) {
            InviteReceivedEvent event = (InviteReceivedEvent) e;
            if(BlackList.contains(event.getAuthor())) {
                return;
            }
            MessageChannel channel = event.isPrivate() ? event.getJDA().getPrivateChannelById(event.getMessage().getChannelId()) :
                    event.getJDA().getTextChannelById(event.getMessage().getChannelId());
            if((event.isPrivate() || event.getMessage().getMentionedUsers().contains(event.getJDA().getSelfInfo()))) {
                if(event.getJDA().getGuildById(event.getInvite().getGuildId()) != null) {
                    try {
                        channel.sendMessage("Already in that Server!");
                    } catch (RuntimeException ignored) {} //no write perms or blocked pm
                    return;
                }
                if(Statics.OAUTH_ID.trim().length() == 0) {
                    channel.sendMessageAsync("I am currently not configured to accept invites!", null);
                } else {
                    channel.sendMessageAsync("I can no longer be invited via invite-links! " +
                            "Please use following link to invite me to your server (manage_server permission required):\n" +
                            "https://discordapp.com/oauth2/authorize?client_id=" + Statics.OAUTH_ID + "&scope=bot", null);
                }
            }
        }
    }
} public class MessageListener implements EventListener {
    @Override
    public void onEvent(Event event) {
        if(event.getClass() == MessageReceivedEvent.class) {
            MessageEvent e = new MessageEvent(((MessageReceivedEvent) event).getMessage(), event.getResponseNumber());
            CommandRegistry.handle(e);
        } else if(event.getClass() == MessageUpdateEvent.class) {
            MessageEvent e = new MessageEvent(((MessageUpdateEvent) event).getMessage(), event.getResponseNumber());
            CommandRegistry.handle(e);
        }
    }
} public class StatusListener implements EventListener {
    @Override
    public void onEvent(Event event) {
        if(event.getClass() == ReadyEvent.class) {
            onReady((ReadyEvent) event);
        } else if(event.getClass() == ReconnectedEvent.class) {
            onReconnect((ReconnectedEvent) event);
        } else if(event.getClass() == GuildJoinEvent.class) {
            onGuildJoin((GuildJoinEvent) event);
        } else if(event.getClass() == GuildLeaveEvent.class) {
            onGuildLeave((GuildLeaveEvent) event);
        } else if(event.getClass() == UserNameUpdateEvent.class) {
            DbEngine.updateUser(((UserNameUpdateEvent) event).getUser());
        }
    }

    public void onReady(ReadyEvent event) {
        initVars(event.getJDA());
    }

    public void onReconnect(ReconnectedEvent event) {
        initVars(event.getJDA());
    }

    private void initVars(JDA jda) {
        jda.getAccountManager().setGame("JDA");
        CommandRegistry.serverConfigs.clear();
        for(Guild guild : jda.getGuilds()) {
            CommandRegistry.serverConfigs.put(guild.getId(), new ServerConfig(jda, guild));
        }
        Statics.botOwner = jda.getUserById(BotConfig.get("ownerId"));
        updateCarbon();
    }

    public void onGuildJoin(GuildJoinEvent event) {
        Statics.LOG.info("Joined Guild " + event.getGuild().getName());
        CommandRegistry.serverConfigs.put(event.getGuild().getId(), new ServerConfig(event.getJDA(), event.getGuild()));
        updateCarbon();
    }

    public void onGuildLeave(GuildLeaveEvent event) {
        Statics.LOG.info("Left Guild " + event.getGuild().getName());
        CommandRegistry.serverConfigs.remove(event.getGuild().getId());
        updateCarbon();
    }

    private static void updateCarbon() {
        String carbonKey = BotConfig.get("carbonKey");
        if(carbonKey == null || carbonKey.trim().isEmpty()) {
            return;
        }
        try {
            Unirest.post("https://www.carbonitex.net/discord/data/botdata.php").field("key", carbonKey).field("servercount", Statics.jdaInstance.getGuilds().size()).asString();
        } catch(UnirestException e) {
            e.printStackTrace();
        }
    }
} public abstract class Module {

    private static Map<String, Class<? extends Module>> modules = new HashMap<>();
    private static Set<String> moduleList = new HashSet<>();

    public static Map<String, Class<? extends Module>> getModules() {
        return modules;
    }

    public static Set<String> getModuleList() {
        return moduleList;
    }

    public static void init() {
        ClassEnumerator.getClassesForPackage(Module.class.getPackage()).stream().filter(aClass -> Module.class.isAssignableFrom(aClass) && !aClass.equals(Module.class)).forEach(aClass -> {
            @SuppressWarnings("unchecked")
            Class<? extends Module> module = (Class<? extends Module>) aClass;
            register(module);
        });
    }

    /**
     * Used to manually register Modules. (Modules in the com.kantenkugel.discordbot.modules package are automatically loaded when init is called)
     * @param moduleClass
     *      the module to register
     */
    public static void register(Class<? extends Module> moduleClass) {
        try {
            Module module = moduleClass.newInstance();
            modules.putIfAbsent(module.getName().toLowerCase(), moduleClass);
            if(!module.hideModule()) {
                moduleList.add(module.getName().toLowerCase());
            }
            if(module.availableInPms()) {
                ServerConfig.PMConfig.registerModule(module.getName().toLowerCase());
            }
            Statics.LOG.info("Registered module " + module.getName().toLowerCase());
        } catch(InstantiationException | IllegalAccessException e) {
            e.printStackTrace();
        }
    }

    /**
     * Get the name of this module. The name should be all lowercase.
     * This name is used as key for enabling/disabling/configuring the module
     *
     * @return
     *      this modules name
     */
    public abstract String getName();

    /**
     * Defines if this modules is available in PMs
     *
     * @return
     *      whether or not this should be available in pms
     */
    public abstract boolean availableInPms();

    /**
     * Initializes this module. This is called after {@link #fromJson(JSONObject)}.
     *
     * @param jda
     *      the JDA object
     * @param cfg
     *      the ServerConfig object
     */
    public abstract void init(JDA jda, ServerConfig cfg);

    /**
     * Pass-through of EVERY MessageEvent
     * @param event
     *      the pass-through event
     * @return
     *      true, if further handling via commands should be stopped
     */
    public boolean handle(MessageEvent event, ServerConfig cfg) { return false; }

    /**
     * Called when the Guild-owner tries to configure this module.
     * If changes were made to the configuration, you should call cfg.save()
     *
     * @param cfgString
     *      null if no config string is supplied (help), or the string supplied to configure
     * @param event
     *      the complete MessageEvent
     * @param cfg
     *      the ServerConfig Object
     */
    public abstract void configure(String cfgString, MessageEvent event, ServerConfig cfg);

    /**
     * This method should return all Commands available via this Module.
     * This is only called when the list of modules in a guild is refreshed and can therefore recalc the Map each time without major performance impact
     *
     * @return
     *      the Map of all available commands (with the key being the command name)
     */
    public abstract Map<String, Command> getCommands();

    /**
     * Should return the Configuration-Object for this module.
     * This method is either called when the server-config is saved, or when this module is first added to a server.
     * Therefore this method should return the default Configuration Object, if this module didn't go through the fromJson+init progress already
     *
     * @return
     *      the current Configuration, or default Configuration, if module didn't get initialized
     */
    public abstract JSONObject toJson();

    /**
     * This is getting called when this module is loaded.
     * The given JsonObject is the current Current configuration Object of this module in the server.
     * This is always called before {@link #init(JDA, ServerConfig)}
     *
     * @param cfg
     *      the current Configuration
     */
    public abstract void fromJson(JSONObject cfg);

    /**
     * Currently unused, but should unload resources from this module (called once this module is removed from the last guild)
     */
    public void unload() {
    }

    /**
     * Used to hide modules (don't show them as available)
     *
     * @return
     *      True - if this module should be hidden
     */
    public boolean hideModule() {
        return false;
    }
} public class DocParser {
    private static final SimpleLog LOG = SimpleLog.getLog("DocParser");

    private static final String JENKINS_PREFIX = "http://ci.dv8tion.net/job/JDA/lastSuccessfulBuild/";
    private static final String ARTIFACT_SUFFIX = "api/json?tree=artifacts[*]";

    private static final Path LOCAL_SRC_PATH = Paths.get("src.jar");

    private static final String JDA_CODE_BASE = "net/dv8tion/jda";

    private static final Pattern DOCS_PATTERN = Pattern.compile("/\\*{2}\\s*\n(.*?)\n\\s*\\*/\\s*\n\\s*(?:@[^\n]+\n\\s*)*(.*?)\n", Pattern.DOTALL);
    private static final Pattern METHOD_PATTERN = Pattern.compile(".*?\\s([a-zA-Z][a-zA-Z0-9]*)\\(([a-zA-Z0-9\\s\\.,<>]*)\\)");
    private static final Pattern METHOD_ARG_PATTERN = Pattern.compile("([a-zA-Z][a-zA-Z0-9<>]*(?:\\.{3})?)\\s+[a-zA-Z][a-zA-Z0-9]");

    private static final String LINK_PATTERN = "\\{@link\\s.*?\\.?([^\\s\\.]+(?:\\([^\\)]*?\\))?)\\}";

    private static final Map<String, List<Documentation>> docs = new HashMap<>();


    public static void init() {
        if(!docs.isEmpty())
            return;
        LOG.info("Initializing JDA-Docs");
        download();
        parse();
        LOG.info("JDA-Docs initialized");
    }

    public static void reFetch() {
        LOG.info("Re-fetching Docs");
        download();
        synchronized(docs) {
            docs.clear();
            parse();
        }
        LOG.info("Done");
    }

    public static String get(String name) {
        String[] split = name.toLowerCase().split("[#\\.]", 2);
        if(split.length != 2)
            return "Incorrect Method declaration";
        List<Documentation> methods;
        synchronized(docs) {
            if(!docs.containsKey(split[0]))
                return "Class not Found!";
            methods = docs.get(split[0]);
        }
        methods = methods.parallelStream().filter(doc -> doc.matches(split[1])).sorted(Comparator.comparingInt(doc -> doc.argTypes.size())).collect(Collectors.toList());
        if(methods.size() == 0)
            return "Method not found/documented in Class!";
        if(methods.size() > 1 && methods.get(0).argTypes.size() != 0)
            return "Multiple methods found: " + methods.parallelStream().map(m -> '(' + StringUtils.join(m.argTypes, ", ") + ')').collect(Collectors.joining(", "));
        Documentation doc = methods.get(0);
        StringBuilder b = new StringBuilder();
        b.append("```\n").append(doc.functionHead).append("\n```\n").append(doc.desc);
        if(doc.args.size() > 0) {
            b.append('\n').append('\n').append("**Arguments:**");
            doc.args.entrySet().stream().map(e -> "**" + e.getKey() + "** - " + e.getValue()).forEach(a -> b.append('\n').append(a));
        }
        if(doc.returns != null)
            b.append('\n').append('\n').append("**Returns:**\n").append(doc.returns);
        if(doc.throwing.size() > 0) {
            b.append('\n').append('\n').append("**Throws:**");
            doc.throwing.entrySet().stream().map(e -> "**" + e.getKey() + "** - " + e.getValue()).forEach(a -> b.append('\n').append(a));
        }
        return b.toString();
    }


    private static void download() {
        LOG.info("Downloading source-file");
        try {
            HttpResponse<String> response = Unirest.get(JENKINS_PREFIX + ARTIFACT_SUFFIX).asString();
            if(response.getStatus() < 300 && response.getStatus() > 199) {
                JSONArray artifacts = new JSONObject(response.getBody()).getJSONArray("artifacts");
                for(int i = 0; i < artifacts.length(); i++) {
                    JSONObject artifact = artifacts.getJSONObject(i);
                    if(artifact.getString("fileName").endsWith("sources.jar")) {
                        URL artifactUrl = new URL(JENKINS_PREFIX + "artifact/" + artifact.getString("relativePath"));
                        InputStream is = artifactUrl.openStream();
                        Files.copy(is, LOCAL_SRC_PATH, StandardCopyOption.REPLACE_EXISTING);
                        is.close();
                        LOG.info("Done downloading source-file");
                    }
                }
            }
        } catch(UnirestException | IOException e) {
            LOG.log(e);
        }
    }

    private static void parse() {
        LOG.info("Parsing source-file");
        try {
            JarFile file = new JarFile(LOCAL_SRC_PATH.toFile());
            file.stream()
                    .filter(entry -> !entry.isDirectory() && entry.getName().startsWith(JDA_CODE_BASE) && entry.getName().endsWith(".java"))
                    .forEach(entry -> {
                        try {
                            parse(entry.getName(), file.getInputStream(entry));
                        } catch(IOException e) {
                            e.printStackTrace();
                        }
                    });
            LOG.info("Done parsing source-file");
        } catch(IOException e) {
            LOG.log(e);
        }
    }

    private static void parse(String name, InputStream inputStream) {
        String[] nameSplits = name.split("[/\\.]");
        String className = nameSplits[nameSplits.length - 2];
        docs.putIfAbsent(className.toLowerCase(), new ArrayList<>());
        List<Documentation> docs = DocParser.docs.get(className.toLowerCase());
        try (BufferedReader buffer = new BufferedReader(new InputStreamReader(inputStream))) {
            String content = buffer.lines().collect(Collectors.joining("\n"));
            Matcher matcher = DOCS_PATTERN.matcher(content);
            while(matcher.find()) {
                String method = matcher.group(2).trim();
                if(method.contains("class ") || method.contains("interface ")) {
                    continue;
                }
                if(method.endsWith("{"))
                    method = method.substring(0, method.length() - 1).trim();
                Matcher m2 = METHOD_PATTERN.matcher(method);
                if(!m2.find())
                    continue;
                String methodName = m2.group(1);
                List<String> argTypes = new ArrayList<>();
                m2 = METHOD_ARG_PATTERN.matcher(m2.group(2));
                while(m2.find())
                    argTypes.add(m2.group(1));
                List<String> docText = cleanupDocs(matcher.group(1));
                String returns = null;

                Map<String, String> args = new HashMap<>();
                Map<String, String> throwing = new HashMap<>();
                String desc = null;
                for(String line : docText) {
                    if(!line.isEmpty() && line.charAt(0) == '@') {
                        if(line.startsWith("@return "))
                            returns = line.substring(8);
                        else if(line.startsWith("@param ")) {
                            String[] split = line.split("\\s+", 3);
                            args.put(split[1], split.length == 3 ? split[2] : "*No Description*");
                        } else if(line.startsWith("@throws ")) {
                            String[] split = line.split("\\s+", 3);
                            throwing.put(split[1], split.length == 3 ? split[2] : "*No Description*");
                        }
                    } else {
                        desc = desc == null ? line : desc + '\n' + line;
                    }
                }
                docs.add(new Documentation(methodName, argTypes, method, desc, returns, args, throwing));
            }
        } catch(IOException ignored) {}
        try {
            inputStream.close();
        } catch(IOException e) {
            LOG.log(e);
        }
    }

    private static List<String> cleanupDocs(String docs) {
        docs = docs.replace("\n", " ");
        docs = docs.replaceAll("(?:\\s+\\*)+\\s+", " ").replaceAll("\\s{2,}", " ");
        docs = docs.replaceAll("</?b>", "**").replaceAll("</?i>", "*").replaceAll("<br/?>", "\n").replaceAll("<[^>]+>", "");
        docs = docs.replaceAll("[^{]@", "\n@");
        docs = docs.replaceAll(LINK_PATTERN, "***$1***");
        return Arrays.stream(docs.split("\n")).map(String::trim).collect(Collectors.toList());
    }

    private static class Documentation {
        private final String functionName;
        private final List<String> argTypes;
        private final String functionHead;
        private final String desc;
        private final String returns;
        private final Map<String, String> args;
        private final Map<String, String> throwing;

        private Documentation(String functionName, List<String> argTypes, String functionHead, String desc, String returns, Map<String, String> args, Map<String, String> throwing) {
            this.functionName = functionName;
            this.argTypes = argTypes;
            this.functionHead = functionHead;
            this.desc = desc;
            this.returns = returns;
            this.args = args;
            this.throwing = throwing;
        }

        private boolean matches(String input) {
            if(input.charAt(input.length()-1) != ')')
                input += "()";
            Matcher matcher = METHOD_PATTERN.matcher(' ' + input);
            if(!matcher.find())
                return false;
            if(!matcher.group(1).equalsIgnoreCase(functionName))
                return false;
            String args = matcher.group(2);
            if(args.isEmpty())
                return true;
            String[] split = args.split(",");
            if(split.length != argTypes.size())
                return false;
            for(int i = 0; i < split.length; i++) {
                if(!split[i].trim().equalsIgnoreCase(argTypes.get(i)))
                    return false;
            }
            return true;
        }
    }
} public class UpdateValidator extends Thread {
    private static final int MAX_TIME = 30*1000;
    private static UpdateValidator instance;
    public static synchronized UpdateValidator getInstance() {
        if(instance == null) {
            instance = new UpdateValidator();
        }
        return instance;
    }

    private UpdateValidator() {
        setDaemon(true);
    }

    @Override
    public void run() {
        try {
            Thread.sleep(MAX_TIME);
        } catch(InterruptedException e) {
            return;
        }
        Statics.LOG.fatal("Failed to start, reverting to old version");
        System.err.flush();
        MiscUtil.shutdown(Statics.REVERT_EXIT_CODE);
    }
} public class UpdateWatcher extends Thread {
    private final JDA api;

    public UpdateWatcher(JDA api) {
        this.api = api;
        setDaemon(true);
        start();
    }

    @Override
    public void run() {
        BufferedReader reader = new BufferedReader(new InputStreamReader(System.in));
        while(true) {
            try {
                String cmd = reader.readLine();
                int code;
                try {
                    code = Integer.parseInt(cmd);
                } catch(NumberFormatException e) {
                    Statics.LOG.warn("Got unknown input via System.in: " + cmd);
                    continue;
                }
                cmd = null;
                switch(code) {
                    case Statics.UPDATE_EXIT_CODE:
                        cmd = "update";
                    case Statics.NORMAL_EXIT_CODE:
                        if(cmd == null)
                            cmd = "shutdown";
                    case Statics.RESTART_EXIT_CODE:
                        if(cmd == null)
                            cmd = "restart";
                        getChannel(api).sendMessage("Wrapper requested to " + cmd + ". Doing so now...");
                        MiscUtil.shutdown(code);
                        return;
                    default:
                        Statics.LOG.warn("UpdateWatcher got unknown command-code " + code + "... ignoring");
                }
            } catch(IOException e) {
                e.printStackTrace();
            }
        }
    }

    public static MessageChannel getChannel(JDA api) {
        return Statics.botOwner.getPrivateChannel();
    }
}
import com.kantenkugel.discordbot.commands.CommandRegistry;
import com.kantenkugel.discordbot.config.BotConfig;
import com.kantenkugel.discordbot.listener.DbListener;
import com.kantenkugel.discordbot.listener.InviteListener;
import com.kantenkugel.discordbot.listener.MessageListener;
import com.kantenkugel.discordbot.listener.StatusListener;
import com.kantenkugel.discordbot.modules.Module;
import com.kantenkugel.discordbot.moduleutils.DocParser;
import com.kantenkugel.discordbot.util.UpdateValidator;
import com.kantenkugel.discordbot.util.UpdateWatcher;
import net.dv8tion.jda.JDABuilder;
import net.dv8tion.jda.events.Event;
import net.dv8tion.jda.events.ReadyEvent;
import net.dv8tion.jda.hooks.EventListener;
import net.dv8tion.jda.utils.SimpleLog;
import org.apache.commons.lang3.StringUtils;
import javax.security.auth.login.LoginException;
import java.io.File;
import java.io.IOException;
/*
 * Copyright 2016 Michael Ritter (Kantenkugel)
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package com.kantenkugel.discordbot;



/**
 * Created by Michael Ritter on 05.12.2015.
 */
public class Main {
    public static UpdateValidator checker = null;

    /*
    Args:
        0  bot-token
        1  system-time of wrapper-start (for uptime),
           OR:
           "true" -> 2nd bot for only db (rest of params ignored)
        2  success-indicator (true/false/"-")
        3  version-number
        4+ MULTIPLE/NONE strings describing the changelog of this version
    */
    public static void main(String[] args) {
        boolean isDbBot = false;
        if(args.length == 2 && Boolean.parseBoolean(args[1])) {
            isDbBot = true;
        } else if(args.length < 4) {
            System.out.println("Missing arguments!");
            return;
        }

        if(!BotConfig.load()) {
            System.out.println("Bot config created/updated. Please populate/check it before restarting the Bot");
            System.exit(Statics.NORMAL_EXIT_CODE);
        }

        if(BotConfig.get("logToFiles", true)) {
            try {
                SimpleLog.addFileLogs(new File("logs/main.txt"), new File("logs/err.txt"));
            } catch(IOException e) {
                e.printStackTrace();
            }
        }

        if(!isDbBot) {
            Statics.START_TIME = Long.parseLong(args[1]);
            Statics.VERSION = Integer.parseInt(args[3]);
        }

        if(!isDbBot && args.length > 4) {
            Statics.CHANGES = StringUtils.join(args, '\n', 4, args.length);
        } else {
            Statics.CHANGES = null;
        }

        if(isDbBot) {
            if("".equals(BotConfig.get("historyBase"))) {
                Statics.LOG.fatal("Please specify a history-base in the config");
                System.exit(Statics.NORMAL_EXIT_CODE);
            }
            if(!DbEngine.init()) {
                Statics.LOG.fatal("Could not connect to db! shutting down");
                System.exit(Statics.NORMAL_EXIT_CODE);
            }
        } else {
            DocParser.init();
            Module.init();
        }
        try {
            JDABuilder jdaBuilder = new JDABuilder().setBotToken(args[0]).setAudioEnabled(false);
            if(isDbBot)
                jdaBuilder.addListener(new DbListener());
            else
                jdaBuilder.addListener(new StatusListener()).addListener(new InviteListener()).addListener(new MessageListener());