public abstract class AbstractCommand implements Command {

    private final static Logger LOG = LoggerFactory.getLogger(AbstractCommand.class);

    protected String name;
    protected String pattern;
    protected DiscordException badUse;
    private boolean isPublic;
    private boolean isUsableInMP;
    private boolean isAdmin;
    private boolean isHidden;


    protected AbstractCommand(String name, String pattern){
        super();
        this.name = name;
        this.pattern = pattern;
        this.isPublic = true;
        this.isUsableInMP = true;
        this.isAdmin = false;
        this.isHidden = false;
        badUse = new BadUseCommandDiscordException();
    }

    @Override
    public final void request(MessageCreateEvent event, Message message) {
        Language lg = Translator.getLanguageFrom(message.getChannel().block());
        try {
            Matcher m = getMatcher(message);
            boolean isFound = m.find();

            // Caché si la fonction est désactivée/réservée aux admin et que l'auteur n'est pas super-admin
            if ((!isPublic() || isAdmin()) && message.getAuthor()
                    .map(user -> user.getId().asLong() != Constants.authorId).orElse(false))
                return;

            // S'il s'agit d'une demande d'aide...
            if (message.getContent().matches(Pattern.quote(getPrefix(message)) + getName() + "\\s+help")){
                message.getChannel().flatMap(chan -> chan
                        .createMessage(helpDetailed(lg, getPrefix(message)))).subscribe();
                return;
            }

            // La commande est trouvée
            if (isFound) {
                // Mais n'est pas utilisable en MP
                MessageChannel channel = message.getChannel().block();
                if (!isUsableInMP() && channel instanceof PrivateChannel) {
                    BasicDiscordException.NOT_USABLE_IN_MP.throwException(message, this, lg);
                    return;
                }
                // Mais est désactivée par la guilde
                else if (!(channel instanceof PrivateChannel) && message.getAuthor()
                        .map(user -> user.getId().asLong() != Constants.authorId).orElse(false)
                        && isForbidden(Guild.getGuild(message.getGuild().block()))) {
                    BasicDiscordException.COMMAND_FORBIDDEN.throwException(message, this, lg);
                    return;
                }
            }
            // Mais est mal utilisée
            else if (message.getContent().startsWith(getPrefix(message) + getName())) {
                badUse.throwException(message, this, lg);
                return;
            }
            if (isFound)
                request(event, message, m, lg);
        } catch(Exception e){
            BasicDiscordException.UNKNOWN_ERROR.throwException(message, this, lg);
            Reporter.report(e);
            LOG.error("request", e);
        }
    }

    /**
     * @param message Message from the event
     * @param m Matcher that permit to fetch data
     * @param lg Language of the channel (FR, EN, ES..)
     */
    protected abstract void request(MessageCreateEvent event, Message message, Matcher m, Language lg);

    @Override
    public boolean isForbidden(Guild g){
        return g.getForbiddenCommands().containsKey(getName());
    }

    @Override
    public Matcher getMatcher(Message message){
        String prefixe = getPrefix(message);
        return Pattern.compile("^" + Pattern.quote(prefixe) + name + pattern + "$")
                .matcher(message.getContent());
    }

    public static String getPrefix(Message message){
        String prefix = "";
        if (message.getChannel().block() instanceof GuildMessageChannel)
            prefix = Guild.getGuild(message.getGuild().block()).getPrefix();
        return prefix;
    }

    protected String getPrefixMdEscaped(Message message){
        if (!(message.getChannel().block() instanceof PrivateChannel))
            return Guild.getGuild(message.getGuild().block()).getPrefix()
                .replaceAll("\\*", "\\\\*")      // Italic & Bold
                .replaceAll("_", "\\_")          // Underline
                .replaceAll("~", "\\~")          // Strike
                .replaceAll("\\`", "\\\\`");     // Code
        return "";
    }

    /**
     * @param message message d'origine
     * @return true si les permissions sont suffisantes, false le cas échéant
     */
    protected boolean isChannelHasExternalEmojisPermission(Message message){
        return message.getChannel().blockOptional().filter(messageChannel -> messageChannel instanceof PrivateChannel ||
                ((TextChannel) messageChannel).getEffectivePermissions(message.getClient().getSelfId())
                        .blockOptional().orElse(PermissionSet.none())
                        .contains(Permission.USE_EXTERNAL_EMOJIS)).isPresent();
    }

    /**
     * Retourne true si l'utilisateur a les droits nécessaires, false le cas échéant
     * @param message Message reçu
     * @return true si l'utilisateur a les droits nécessaires, false le cas échéant
     */
    protected boolean isUserHasEnoughRights(Message message){
        return message.getChannel().blockOptional()
                .filter(messageChannel -> !(messageChannel instanceof PrivateChannel) && (message.getAuthor()
                        .map(user -> user.getId().asLong() == Constants.authorId).orElse(false)
                || ((GuildMessageChannel) messageChannel).getEffectivePermissions(message.getAuthor()
                        .map(User::getId).orElse(Snowflake.of(0L))).blockOptional().orElse(PermissionSet.none())
                .contains(Permission.MANAGE_GUILD))).isPresent();
    }

    @Override
    public String getName() {
        return name;
    }

    @Override
    public String getPattern() {
        return pattern;
    }

    @Override
    public void setPublic(boolean isPublic){
        this.isPublic = isPublic;
    }

    @Override
    public boolean isPublic(){ return isPublic; }

    @Override
    public boolean isUsableInMP(){ return isUsableInMP; }

    @Override
    public void setUsableInMP(boolean isUsableInMP) {
        this.isUsableInMP = isUsableInMP;
    }

    @Override
    public boolean isAdmin() {
        return isAdmin;
    }

    @Override
    public void setAdmin(boolean isAdmin) {
        this.isAdmin = isAdmin;
    }

    @Override
    public boolean isHidden() {
        return isHidden;
    }

    @Override
    public void setHidden(boolean isHidden) {
        this.isHidden = isHidden;
    }
} public class Guild {

    private final static Logger LOG = LoggerFactory.getLogger(Guild.class);
    private static Map<String, Guild> guilds;
    private String id;
    private String name;
    private Map<String, CommandForbidden> commands;
    private String prefix;
    private ServerDofus server;
    private Language language;

    public Guild(String id, String name, Language lang){
        this(id, name, Constants.prefixCommand, lang, null);
    }

    private Guild(String id, String name, String prefix, Language lang, String serverDofus){
        this.id = id;
        this.name = name;
        this.prefix = prefix;
        commands = CommandForbidden.getForbiddenCommands(this);
        this.language = lang;
        this.server = ServerDofus.getServersMap().get(serverDofus);
    }

    public synchronized void addToDatabase(){
        if (! getGuilds().containsKey(id)){
            getGuilds().put(id, this);

            Connexion connexion = Connexion.getInstance();
            Connection connection = connexion.getConnection();

            try {
                PreparedStatement request = connection.prepareStatement("INSERT INTO"
                        + " Guild(id, name, prefixe, lang) VALUES (?, ?, ?, ?);");
                request.setString(1, id);
                request.setString(2, name);
                request.setString(3, prefix);
                request.setString(4, language.getAbrev());
                request.executeUpdate();
            } catch (SQLException e) {
                Reporter.report(e);
                LOG.error(e.getMessage());
            }
        }
    }

    public synchronized void removeToDatabase() {
        if (getGuilds().containsKey(id)) {
            getGuilds().remove(id);

            Connexion connexion = Connexion.getInstance();
            Connection connection = connexion.getConnection();

            try {
                PreparedStatement request = connection.prepareStatement("DELETE FROM Guild WHERE ID = ?;");
                request.setString(1, id);
                request.executeUpdate();

            } catch (SQLException e) {
                Reporter.report(e);
                LOG.error(e.getMessage());
            }
        }
    }

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

        Connexion connexion = Connexion.getInstance();
        Connection connection = connexion.getConnection();

        try {
            PreparedStatement preparedStatement = connection.prepareStatement(
                    "UPDATE Guild SET name = ? WHERE id = ?;");
            preparedStatement.setString(1, name);
            preparedStatement.setString(2, id);
            preparedStatement.executeUpdate();
        } catch (SQLException e) {
            Reporter.report(e);
            LOG.error(e.getMessage());
        }
    }

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

        Connexion connexion = Connexion.getInstance();
        Connection connection = connexion.getConnection();

        try {
            PreparedStatement preparedStatement = connection.prepareStatement(
                    "UPDATE Guild SET prefixe = ? WHERE id = ?;");
            preparedStatement.setString(1, prefix);
            preparedStatement.setString(2, id);
            preparedStatement.executeUpdate();
        } catch (SQLException e) {
            Reporter.report(e);
            LOG.error(e.getMessage());
        }
    }

    public synchronized void setServer(ServerDofus server){
        this.server = server;

        Connexion connexion = Connexion.getInstance();
        Connection connection = connexion.getConnection();

        try {
            PreparedStatement preparedStatement = connection.prepareStatement(
                    "UPDATE Guild SET server_dofus = ? WHERE id = ?;");
            if (server != null) preparedStatement.setString(1, server.getName());
            else preparedStatement.setNull(1, Types.VARCHAR);
            preparedStatement.setString(2, id);

            preparedStatement.executeUpdate();
        } catch (SQLException e) {
            Reporter.report(e);
            LOG.error(e.getMessage());
        }
    }

    public synchronized void setLanguage(Language lang){
        this.language = lang;

        Connexion connexion = Connexion.getInstance();
        Connection connection = connexion.getConnection();

        try {
            PreparedStatement preparedStatement = connection.prepareStatement(
                    "UPDATE Guild SET lang = ? WHERE id = ?;");
            preparedStatement.setString(1, lang.getAbrev());
            preparedStatement.setString(2, id);

            preparedStatement.executeUpdate();
        } catch (SQLException e) {
            Reporter.report(e);
            LOG.error(e.getMessage());
        }
    }

    public synchronized static Map<String, Guild> getGuilds(){
        if (guilds == null){
            guilds = new ConcurrentHashMap<>();
            String id;
            String name;
            String prefix;
            String server;
            String lang;

            Connexion connexion = Connexion.getInstance();
            Connection connection = connexion.getConnection();

            try {
                PreparedStatement query = connection.prepareStatement("SELECT id, name, prefixe, server_dofus, lang FROM Guild");
                ResultSet resultSet = query.executeQuery();

                while (resultSet.next()) {
                    id = resultSet.getString("id");
                    name = resultSet.getString("name");
                    prefix = resultSet.getString("prefixe");
                    server = resultSet.getString("server_dofus");
                    lang = resultSet.getString("lang");

                    guilds.put(id, new Guild(id, name, prefix, Language.valueOf(lang), server));
                }
            } catch (SQLException e) {
                Reporter.report(e);
                LOG.error(e.getMessage());
            }
        }
        return guilds;
    }

    public static Guild getGuild(discord4j.core.object.entity.Guild guild){
        return getGuild(guild, true);
    }

    public synchronized static Guild getGuild(discord4j.core.object.entity.Guild discordGuild, boolean forceCache){
        Guild guild = getGuilds().get(discordGuild.getId().asString());

        if (guild == null && forceCache){
            guild = new Guild(discordGuild.getId().asString(), discordGuild.getName(), Constants.defaultLanguage);
            guild.addToDatabase();
        }

        return guild;
    }

    public String getId(){
        return id;
    }

    public String getName(){
        return name;
    }

    public String getPrefix(){ return prefix; }

    public Language getLanguage() {
        return language;
    }

    public Map<String, CommandForbidden> getForbiddenCommands(){
        return commands;
    }

    public ServerDofus getServerDofus() {
        return server;
    }
} public class JobUser extends ObjectUser {

    private final static Logger LOG = LoggerFactory.getLogger(JobUser.class);
    private static final String JOB_PREFIX = "job";
    private static final int NUMBER_FIELD = 3;
    private static MultiKeySearch<JobUser> jobs;
    private Job job;

    public JobUser(long idUser, ServerDofus server, Job job, int level){
        this.job = job;
        if (level > 200)
            level = 200;
        this.level = level;
        this.idUser = idUser;
        this.server = server;
    }

    public synchronized void setLevel(int level){
        if (level > 200)
            level = 200;
        this.level = level;

        Connexion connexion = Connexion.getInstance();
        Connection connection = connexion.getConnection();

        try {
            PreparedStatement preparedStatement;
            if (level > 0) {
                preparedStatement = connection.prepareStatement(
                        "UPDATE Job_User SET level = ?"
                                + "WHERE name_job = ? AND id_user = ? AND server_dofus = ?;");
                preparedStatement.setInt(1, level);
                preparedStatement.setString(2, job.getName());
                preparedStatement.setString(3, String.valueOf(idUser));
                preparedStatement.setString(4, server.getName());
            }
            else {
                preparedStatement = connection.prepareStatement(
                        "DELETE FROM Job_User WHERE name_job = ? AND id_user = ? AND server_dofus = ?;");
                preparedStatement.setString(1, job.getName());
                preparedStatement.setString(2, String.valueOf(idUser));
                preparedStatement.setString(3, server.getName());
                getJobs().remove(idUser, server, job);
            }

            preparedStatement.executeUpdate();
        } catch (SQLException e) {
            LOG.error("setLevel", e);
        }
    }

    /**
     * Ajoute à la base de donnée l'objet si celui-ci n'y est pas déjà.
     */
    public synchronized void addToDatabase(){

        if (! getJobs().containsKeys(idUser, server, job) && level > 0) {
            getJobs().add(this, idUser, server, job);
            Connexion connexion = Connexion.getInstance();
            Connection connection = connexion.getConnection();

            try {
                PreparedStatement preparedStatement = connection.prepareStatement(
                        "INSERT INTO Job_User(name_job, id_user, server_dofus, level) VALUES(?, ?, ?, ?);");
                preparedStatement.setString(1, job.getName());
                preparedStatement.setString(2, String.valueOf(idUser));
                preparedStatement.setString(3, server.getName());
                preparedStatement.setInt(4, level);

                preparedStatement.executeUpdate();
            } catch (SQLException e) {
                LOG.error("addToDatabase", e);
            }
        }
    }

    private Job getJob() {
        return job;
    }

    private static synchronized MultiKeySearch<JobUser> getJobs(){
        if(jobs == null){
            jobs = new MultiKeySearch<>(NUMBER_FIELD);
            Connexion connexion = Connexion.getInstance();
            Connection connection = connexion.getConnection();

            try {
                PreparedStatement query = connection.prepareStatement(
                        "SELECT id_user, server_dofus, name_job, level FROM Job_User;");
                ResultSet resultSet = query.executeQuery();

                while (resultSet.next()) {
                    Long idUser = resultSet.getLong("id_user");
                    ServerDofus server = ServerDofus.getServersMap().get(resultSet.getString("server_dofus"));
                    Job job = Job.getJob(resultSet.getString("name_job"));
                    int level = resultSet.getInt("level");
                    jobs.add(new JobUser(idUser, server, job, level), idUser, server, job);
                }
            } catch (SQLException e) {
                Reporter.report(e);
                LOG.error("getJobs", e);
            }
        }

        return jobs;
    }

    /**
     * @param user Joueur de la guilde
     * @param server Serveur dofus
     * @return Liste des résultats de la recherche
     */
    public static List<EmbedCreateSpec> getJobsFromUser(Member user, ServerDofus server, Language lg){
        List<JobUser> result = getJobs().get(user.getId().asLong(), server, null);
        result.sort(JobUser::compare);

        EmbedCreateSpec.Builder builder = EmbedCreateSpec.builder()
                .title(Translator.getLabel(lg, "job.user").replace("{user}", user.getDisplayName()))
                .thumbnail(user.getAvatarUrl());

        if (! result.isEmpty()) {
            StringBuilder st = new StringBuilder();
            for (JobUser jobUser : result)
                st.append(jobUser.job.getLabel(lg)).append(" : ").append(jobUser.level).append("\n");
            builder.addField(Translator.getLabel(lg, "job.jobs"), st.toString(), true);
        }
        else
            builder.description(Translator.getLabel(lg, "job.empty"));
        builder.footer(server.getLabel(lg), null);

        return Arrays.asList(builder.build());
    }

    /**
     * @param users Joueurs de la guilde
     * @param server Serveur dofus
     * @param jobs Métiers
     * @param level Niveau; si ingérieur à 0, filtre ignoré
     * @return Liste des résultats de la recherche
     */
    public static List<EmbedCreateSpec> getJobsFromFilters(List<Member> users, ServerDofus server, Set<Job> jobs,
                                                             int level, discord4j.core.object.entity.Guild guild, Language lg){
        List<JobUser> result = new ArrayList<>();
        for(Member user : users)
            if (! user.isBot()){
                for(Job job : jobs) {
                    List<JobUser> potentials = getJobs().get(user.getId().asLong(), server, job);
                    if (level > 0) {
                        for (JobUser potential : potentials)
                            if (potential.getLevel() >= level)
                                result.add(potential);
                    } else
                        result.addAll(potentials);
                }
            }
        result.sort(JobUser::compare);
        return getPlayersList(result, guild, server, lg, JOB_PREFIX);
    }

    @Override
    protected String displayLine(discord4j.core.object.entity.Guild guild, Language lg) {
        Optional<Member> member = guild.getMemberById(Snowflake.of(idUser)).blockOptional();

        if (member.isPresent()) {
            return job.getLabel(lg) + ", " + level + " : **"
                    + member.get().getDisplayName() + "**\n";
        }
        return "";
    }

    /**
     * if JobUser instance contains keys
     * @param user User id
     * @param server Dofus server
     * @param job Job (from Job enum)
     * @return true if it contains
     */
    public static boolean containsKeys(long user, ServerDofus server, Job job){
        return getJobs().containsKeys(user, server, job);
    }


    /**
     * getData with all the keys
     * @param user User id
     * @param server Dofus server
     * @param job Job (from Job enum)
     * @return List of JobUser
     */
    public static List<JobUser> get(long user, ServerDofus server, Job job){
        return getJobs().get(user, server, job);
    }

    private static int compare(JobUser jobUser1, JobUser jobUser2){
        return Comparator.comparingInt(JobUser::getLevel).reversed()
                .thenComparing(JobUser::getJob)
                .compare(jobUser1, jobUser2);
    }
} public class ServerDofus {

    private final static Logger LOG = LoggerFactory.getLogger(ServerDofus.class);
    private static List<ServerDofus> servers;
    private static Map<String, ServerDofus> serversMap;
    private static boolean initialized = false;
    private String name;
    private String id;
    private Map<Language, String> labels;
    private Game game;

    public ServerDofus(String name, String id, Game game) {
        this.name = name;
        this.id = id;
        this.game = game;
        this.labels = new HashMap<>();
    }

    private synchronized static void initialize(){
        initialized = true;
        servers = new ArrayList<>();
        serversMap = new HashMap<>();

        Connexion connexion = Connexion.getInstance();
        Connection connection = connexion.getConnection();

        try {
            PreparedStatement query = connection.prepareStatement("SELECT name, id_dofus, game FROM Server");
            ResultSet resultSet = query.executeQuery();

            while (resultSet.next()) {
                ServerDofus sd = new ServerDofus(resultSet.getString("name"),
                        resultSet.getString("id_dofus"),
                        Game.valueOf(resultSet.getString("game")));
                servers.add(sd);
                serversMap.put(sd.getName(), sd);
            }

            query = connection.prepareStatement("SELECT server, language, label FROM Server_Label");
            resultSet = query.executeQuery();
            while (resultSet.next()) {
                ServerDofus sd = serversMap.get(resultSet.getString("server"));
                sd.labels.put(Language.valueOf(resultSet.getString("language")), resultSet.getString("label"));
            }

        } catch (SQLException e) {
            Reporter.report(e);
            LOG.error("initialize", e);
        }
    }

    public synchronized static Map<String, ServerDofus> getServersMap(){
        if (! initialized)
            initialize();
        return serversMap;
    }

    public synchronized static List<ServerDofus> getServersDofus(){
        if (! initialized)
            initialize();
        return servers;
    }

    public String getLabel(Language lang){
        return Optional.ofNullable(labels.get(lang)).orElse(getName());
    }

    public String getName() {
        return name;
    }

    public String getId() {
        return id;
    }

    public Game getGame(){
        return game;
    }

    @Override
    public String toString(){
        return getName();
    }
} public enum Job {

    ALCHIMISTE("job.alchimiste"),
    BIJOUTIER("job.bijoutier"),
    BRICOLEUR("job.bricoleur"),
    BUCHERON("job.bucheron"),
    CHASSEUR("job.chasseur"),
    CORDOMAGE("job.cordomage"),
    CORDONNIER("job.cordonnier"),
    COSTUMAGE("job.costumage"),
    FACONNEUR("job.faconneur"),
    FORGEMAGE("job.forgemage"),
    FORGERON("job.forgeron"),
    JOAILLOMAGE("job.joaillomage"),
    MINEUR("job.mineur"),
    PAYSAN("job.paysan"),
    PECHEUR("job.pecheur"),
    SCULPTEMAGE("job.sculptemage"),
    SCULPTEUR("job.sculpteur"),
    TAILLEUR("job.tailleur"),
    FACOMAGE("job.facomage");

    private static Map<String, Job> jobs;
    private String name;

    Job(String name){this.name = name;}

    public String getName(){
        return name;
    }

    public String getLabel(Language lg){
        return Translator.getLabel(lg, getName());
    }

    /**
     *
     * @param name Nom du métier
     * @return L'énumération Job correspondant à name
     * @throws IllegalArgumentException si aucun Job n'est trouvé pour le nom proposé.
     */
    public static synchronized Job getJob(String name){
        if (jobs == null){
            jobs = new HashMap<>();
            for(Job job : Job.values())
                jobs.put(job.getName(), job);
        }

        if (jobs.containsKey(name))
            return jobs.get(name);
        throw new IllegalArgumentException("No job found for \"" + name + "\".");
    }
} public enum Language {

    FR("Français", "FR"), EN("English", "EN"), ES("Español", "ES");

    private String name;
    private String abrev;

    Language(String name, String abrev){
        this.name = name;
        this.abrev = abrev;
    }

    public String getName() {
        return name;
    }

    public String getAbrev() {
        return abrev;
    }

    @Override
    public String toString(){
        return name;
    }
} public class BasicDiscordException implements DiscordException {

    private final static Logger LOG = LoggerFactory.getLogger(BasicDiscordException.class);
    public final static BasicDiscordException ALMANAX = new BasicDiscordException("exception.basic.almanax");
    public final static BasicDiscordException ALLIANCEPAGE_INACCESSIBLE = new BasicDiscordException("exception.basic.alliancepage_inaccessible");
    public final static BasicDiscordException GUILDPAGE_INACCESSIBLE = new BasicDiscordException("exception.basic.guildpage_inaccessible");
    public final static BasicDiscordException CHARACTERPAGE_INACCESSIBLE = new BasicDiscordException("exception.basic.characterpage_inaccessible");
    public final static BasicDiscordException CHARACTER_TOO_OLD = new BasicDiscordException("exception.basic.character_too_old");
    public final static BasicDiscordException COMMAND_FORBIDDEN = new BasicDiscordException("exception.basic.command_forbidden");
    public final static BasicDiscordException FORBIDDEN_COMMAND_FOUND = new BasicDiscordException("exception.basic.forbidden_command_found");
    public final static BasicDiscordException FORBIDDEN_COMMAND_NOTFOUND = new BasicDiscordException("exception.basic.forbidden_command_notfound");
    public final static BasicDiscordException INCORRECT_DATE_FORMAT = new BasicDiscordException("exception.basic.incorrect_date_format");
    public final static BasicDiscordException IN_DEVELOPPMENT = new BasicDiscordException("exception.basic.in_developpment");
    public final static BasicDiscordException NO_EXTERNAL_EMOJI_PERMISSION = new BasicDiscordException("exception.basic.no_external_emoji_permission");
    public final static BasicDiscordException NO_SEND_TEXT_PERMISSION = new BasicDiscordException("exception.basic.no_send_text_permission");
    public final static BasicDiscordException NO_NSFW_CHANNEL = new BasicDiscordException("exception.basic.no_nsfw_channel");
    public final static BasicDiscordException NO_ENOUGH_RIGHTS = new BasicDiscordException("exception.basic.no_enough_rights");
    public final static BasicDiscordException NO_VOICE_PERMISSION = new BasicDiscordException("exception.basic.no_voice_permission");
    public final static BasicDiscordException NOT_IN_VOCAL_CHANNEL = new BasicDiscordException("exception.basic.not_in_vocal_channel");
    public final static BasicDiscordException NOT_USABLE_IN_MP = new BasicDiscordException("exception.basic.not_usable_in_mp");
    public final static BasicDiscordException UNKNOWN_ERROR = new BasicDiscordException("exception.basic.unknown_error");
    public final static BasicDiscordException USER_NEEDED = new BasicDiscordException("exception.basic.user_needed");
    public final static BasicDiscordException VOICE_CHANNEL_LIMIT = new BasicDiscordException("exception.basic.voice_channel_limit");
    private String messageKey;

    private BasicDiscordException(String message){
        this.messageKey = message;
    }
    @Override
    public void throwException(Message message, Command command, Language lg, Object... arguments) {
        message.getChannel().flatMap(chan -> chan.createMessage(Translator.getLabel(lg, messageKey))).subscribe();
    }
} public interface DiscordException {

    void throwException(Message message, Command command, Language lg, Object... arguments);
} public class NotFoundDiscordException implements DiscordException {

    private String objectKey;

    public NotFoundDiscordException(String objectKey){
        this.objectKey = objectKey;
    }

    @Override
    public void throwException(Message message, Command command, Language lg, Object... arguments) {
        AnkamaBug bug = null;
        if (command instanceof WhoisCommand)
            bug = AnkamaBug.CHARACTER_NOT_FOUND;
        else if (command instanceof GuildCommand)
            bug = AnkamaBug.GUILD_NOT_FOUND;
        else if (command instanceof AllianceCommand)
            bug = AnkamaBug.ALLY_NOT_FOUND;
        else if (command instanceof ItemCommand)
            if (message.getContent().contains("'"))
                bug = AnkamaBug.ITEM_NOT_FOUND_APOSTROPHE;
            else if (message.getContent().toLowerCase()
                    .contains(Translator.getLabel(lg, "equip.muldo").toLowerCase())
                    || message.getContent().toLowerCase()
                    .contains(Translator.getLabel(lg, "equip.volkorne").toLowerCase()))
                bug = AnkamaBug.ITEM_PAGE_MULDO_VOLKORNE_NOT_FOUND;

        String gender = Translator.getLabel(lg, "exception.object." + objectKey + ".gender");
        String text = Translator.getLabel(lg, "exception.notfound.not." + gender)
                + " " + Translator.getLabel(lg, "exception.object." + objectKey + ".singular")
                + " " + Translator.getLabel(lg, "exception.notfound.found." + gender) + ".";

        if (bug != null){
            final AnkamaBug BUG = bug;
            message.getChannel().flatMap(channel -> channel
                    .createEmbed(BUG.decorateEmbed(text, lg)))
                    .subscribe();
        }
        else
            message.getChannel().flatMap(channel -> channel
                    .createMessage(text))
                    .subscribe();
    }
} public final class ServerUtils {

    private static final DiscordException TOO_MANY_SERVERS = new TooMuchDiscordException("server");
    private static final DiscordException NOT_FOUND_SERVER = new NotFoundDiscordException("server");
    private static final DiscordException WRONG_BOT_USED = new WrongBotUsedDiscordException();


    public static ServerDofus getDofusServerFrom(Guild guild, MessageChannel channel){
        return Optional.ofNullable(ChannelServer.getChannelServers().get(channel.getId().asLong()))
                .map(ChannelServer::getServer)
                .orElse(guild.getServerDofus());
    }

    @NotNull
    public static ServerQuery getServerDofusFromName(String proposedServer, Language language){
        ServerQuery result = new ServerQuery();
        final String PROPOSED_SERVER = Normalizer.normalize(proposedServer, Normalizer.Form.NFD)
                .replaceAll("\\p{InCombiningDiacriticalMarks}+", "")
                .replaceAll("\\W+", "").toLowerCase().trim();

        List<ServerDofus> allServers = ServerDofus.getServersDofus().stream()
                .filter(server -> Normalizer.normalize(server.getLabel(language), Normalizer.Form.NFD)
                        .replaceAll("\\p{InCombiningDiacriticalMarks}+", "")
                        .replaceAll("\\W+", "").toLowerCase().trim()
                        .startsWith(PROPOSED_SERVER))
                .collect(Collectors.toList());

        List<ServerDofus> serverDofusList = allServers.stream()
            .filter(server -> server.getGame() == Game.DOFUS)
            .collect(Collectors.toList());

        // Check if the server name is exactly the good one
        List<ServerDofus> exactMatchDofusServer = serverDofusList.stream()
                .filter(server -> Normalizer.normalize(server.getLabel(language), Normalizer.Form.NFD)
                .replaceAll("\\p{InCombiningDiacriticalMarks}+", "")
                .replaceAll("\\W+", "").toLowerCase().trim()
                .equals(PROPOSED_SERVER))
                .collect(Collectors.toList());

        if (exactMatchDofusServer.size() == 1)
            serverDofusList = exactMatchDofusServer;

        if (!serverDofusList.isEmpty()) {
            result.serversFound = serverDofusList;
            if (serverDofusList.size() > 1)
                result.exceptions.add(TOO_MANY_SERVERS);
            else
                result.server = serverDofusList.get(0);
        }
        else if (!allServers.isEmpty()) {
            result.serversFound = allServers;
            result.exceptions.add(WRONG_BOT_USED);
        }
        else
            result.exceptions.add(NOT_FOUND_SERVER);
        return result;
    }

    public static final class ServerQuery {
        private ServerDofus server;
        private List<ServerDofus> serversFound;
        private List<DiscordException> exceptions;

        private ServerQuery(){
            serversFound = new ArrayList<>();
            exceptions = new ArrayList<>();
        }

        public ServerDofus getServer(){
            return server;
        }

        public boolean hasSucceed(){
            return exceptions.isEmpty();
        }

        public List<ServerDofus> getServersFound(){
            return serversFound;
        }

        public List<DiscordException> getExceptions(){
            return exceptions;
        }
    }
} public class Translator {

    private final static Logger LOG = LoggerFactory.getLogger(Translator.class);
    private static final int MAX_MESSAGES_READ = 100;
    private static final int MAX_CHARACTER_ACCEPTANCE = 20;
    private static Map<Language, Properties> labels;
    private static LanguageDetector languageDetector;

    private static LanguageDetector getLanguageDetector(){
        if (languageDetector == null){
            try {
                List<String> languages = new ArrayList<>();
                for(Language lg : Language.values())
                    languages.add(lg.getAbrev().toLowerCase());

                List<LanguageProfile> languageProfiles = new LanguageProfileReader().read(languages);
                languageDetector = LanguageDetectorBuilder.create(NgramExtractors.standard())
                                .withProfiles(languageProfiles).build();
            }
            catch (IOException e) {
                LOG.error("Translator.getLanguageDetector", e);
            }
        }
        return languageDetector;
    }

    /**
     * Fournit la langue utilisée dans un salon textuel
     * @param channel Salon textuel
     * @return Langue de la guilde ou du salon si précisé
     */
    public static Language getLanguageFrom(MessageChannel channel){
        Language result = Constants.defaultLanguage;
        if (channel instanceof GuildMessageChannel) {

            Guild guild = Guild.getGuild(((GuildMessageChannel) channel).getGuild().block());
            result = guild.getLanguage();
            ChannelLanguage channelLanguage = ChannelLanguage.getChannelLanguages().get(channel.getId().asLong());
            if (channelLanguage != null)
                result = channelLanguage.getLang();
        }
        return result;
    }
    public static Language getLanguageFrom(RestChannel channel) {
        Guild guild = Guild.getGuilds().get(channel.getData().block().guildId().get().asString());
        Language result = guild.getLanguage();

        ChannelLanguage channelLanguage = ChannelLanguage.getChannelLanguages().get(channel.getData().block().id().asLong());
        if (channelLanguage != null)
            result = channelLanguage.getLang();

        return result;
    }

    /**
     * Génère une liste de source formatée à partir d'un salon textuel
     * @param channel Salon d'origine
     * @return Liste de message éligibles à une reconnaissance de langue
     */
    private static List<String> getReformatedMessages(MessageChannel channel){
        List<String> result = new ArrayList<>();

        if (channel != null) {
            try {
                List<Message> messages = channel.getMessagesBefore(Snowflake.of(Instant.now()))
                        .take(MAX_MESSAGES_READ).collectList().blockOptional().orElse(Collections.emptyList());
                for (Message message : messages) {
                    String content = message.getContent().replaceAll(":\\w+:", "").trim();
                    if (content.length() > MAX_CHARACTER_ACCEPTANCE)
                        result.add(content);
                }
            } catch (Exception e){
                LOG.warn("Impossible to gather data from " + channel.getId().asString());
            }
        }
        return result;
    }

    private static List<String> getReformatedMessages(RestChannel channel){
        List<String> result = new ArrayList<>();

        if (channel != null) {
            try {
                List<MessageData> messages = channel.getMessagesBefore(Snowflake.of(Instant.now()))
                        .take(MAX_MESSAGES_READ).collectList().blockOptional().orElse(Collections.emptyList());
                for (MessageData message : messages) {
                    String content = message.content().replaceAll(":\\w+:", "").trim();
                    if (content.length() > MAX_CHARACTER_ACCEPTANCE)
                        result.add(content);
                }
            } catch (Exception e){
                LOG.warn("Impossible to gather data from rest channel");
            }
        }
        return result;
    }

    /**
     * Détermine une langue à partir d'une source textuelle
     * @param source Source textuelle
     * @return Langue majoritaire détectée au sein de la source
     */
    private static Language getLanguageFrom(String source){
        TextObject textObject = CommonTextObjectFactories.forDetectingOnLargeText().forText(source);
        LdLocale lang = getLanguageDetector().detect(textObject)
                .or(LdLocale.fromString(Constants.defaultLanguage.getAbrev().toLowerCase()));

        for(Language lg : Language.values())
            if(lang.getLanguage().equals(lg.getAbrev().toLowerCase()))
                return lg;
        return Constants.defaultLanguage;
    }

    /**
     * Détecte la langue majoritaire utilisée dans un salon textuel
     * @param channel Salon textuel à étudier
     * @return Langue majoritaire (ou Constants.defaultLanguage si non trouvée)
     */
    public static Language detectLanguage(GuildMessageChannel channel){
        Language result = Constants.defaultLanguage;
        Map<Language, LanguageOccurrence> languages = new HashMap<>();
        for(Language lang : Language.values())
            languages.put(lang, LanguageOccurrence.of(lang));

        List<String> sources = getReformatedMessages(channel);
        for (String source : sources)
            languages.get(getLanguageFrom(source)).increment();

        int longest = languages.values().stream()
                .mapToInt(LanguageOccurrence::getOccurrence)
                .max()
                .orElse(-1);

        if (longest > 0){
            List<Language> languagesSelected = languages.values().stream()
                    .filter(lo -> lo.getOccurrence() == longest)
                    .map(LanguageOccurrence::getLanguage)
                    .collect(Collectors.toList());
            if (! languagesSelected.contains(result))
                return languagesSelected.get(0);
        }

        return result;
    }

    /**
     * Fournit un libellé dans la langue choisi, pour un code donné
     * @param lang Language du libellé
     * @param property code du libellé
     * @return Libellé correspondant au code, dans la langue choisie
     */
    public synchronized static String getLabel(Language lang, String property){
        if (labels == null){
            labels = new ConcurrentHashMap<>();

            for(Language language : Language.values())
                try(InputStream file = Translator.class.getResourceAsStream("/label_" + language.getAbrev())) {
                    Properties prop = new Properties();
                    prop.load(new BufferedReader(new InputStreamReader(file, StandardCharsets.UTF_8)));
                    labels.put(language, prop);
                } catch (IOException e) {
                    LOG.error("Translator.getLabel", e);
                }
        }

        String value = labels.get(lang).getProperty(property);
        if (value == null || value.trim().isEmpty())
            if (Constants.defaultLanguage != lang) {
                LOG.warn("Missing label in " + lang.getAbrev() + " : " + property);
                return getLabel(Constants.defaultLanguage, property);
            }
            else
                return property;
        return value;
    }

    /**
     * Classe dédiée à la détection de la langue la plus utilisée au sein d'un salon textuel
     */
    private static class LanguageOccurrence {
        private final static LanguageOccurrence DEFAULT_LANGUAGE = of(Constants.defaultLanguage);
        private final static int DEFAULT_VALUE = 0;
        private Language language;
        private int occurrence;

        private LanguageOccurrence(Language language, int occurrence){
            this.language = language;
            this.occurrence = occurrence;
        }

        private static LanguageOccurrence of(Language language){
            return new LanguageOccurrence(language, DEFAULT_VALUE);
        }

        public Language getLanguage(){
            return language;
        }

        private int getOccurrence(){
            return occurrence;
        }

        private void increment(){
            occurrence++;
        }

        public boolean equals(LanguageOccurrence lgo){
            return language.equals(lgo.language);
        }
    }
}
import commands.model.AbstractCommand;
import data.Guild;
import data.JobUser;
import data.ServerDofus;
import discord4j.common.util.Snowflake;
import discord4j.core.event.domain.message.MessageCreateEvent;
import discord4j.core.object.entity.Member;
import discord4j.core.object.entity.Message;
import discord4j.core.spec.EmbedCreateSpec;
import enums.Job;
import enums.Language;
import exceptions.BasicDiscordException;
import exceptions.DiscordException;
import exceptions.NotFoundDiscordException;
import util.ServerUtils;
import util.Translator;
import java.text.Normalizer;
import java.util.*;
import java.util.function.Consumer;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import java.util.stream.Collectors;
package commands.classic;



/**
 * Created by steve on 14/07/2016.
 */
public class JobCommand extends AbstractCommand {

    private final static int MAX_JOB_DISPLAY = 3;

    private DiscordException notFoundServer;

    public JobCommand(){
        super("job", "(.*)");
        setUsableInMP(false);
        notFoundServer = new NotFoundDiscordException("server");
    }

    @Override
    protected void request(MessageCreateEvent event, Message message, Matcher m, Language lg) {
        String content = m.group(1).trim().replaceAll(",", "");

        // Filter Initialisation
        Optional<discord4j.core.object.entity.Guild> guild = message.getGuild().blockOptional();
        Optional<Member> user = message.getAuthorAsMember().blockOptional();

        if (guild.isPresent() && user.isPresent()){
            ServerDofus server = ServerUtils.getDofusServerFrom(Guild.getGuild(guild.get()), message.getChannel().block());

            // Concerned user is the author?
            if(Pattern.compile("^<@[!|&]?\\d+>").matcher(content).find()){
                content = content.replaceFirst("<@[!|&]?\\d+>", "").trim();
                Optional<Snowflake> memberId = message.getUserMentionIds().stream().findFirst();
                if (! memberId.isPresent()){
                    BasicDiscordException.USER_NEEDED.throwException(message, this, lg);
                    return;
                }
                user = guild.get().getMemberById(memberId.get()).blockOptional();
            }

            //user data consultation
            ServerUtils.ServerQuery serverQuery = ServerUtils.getServerDofusFromName(content, lg);
            if (! serverQuery.getServersFound().isEmpty() && Pattern.compile("(.+)").matcher(content).matches()
                    || content.isEmpty()){
                if (serverQuery.hasSucceed())
                    server = serverQuery.getServer();
                else if (server == null) {
                    if (!content.isEmpty())
                        serverQuery.getExceptions()
                                .forEach(e -> e.throwException(message, this, lg, serverQuery.getServersFound()));
                    else
                        notFoundServer.throwException(message, this, lg);
                    return;
                }

                if (user.isPresent()) {
                    List<EmbedCreateSpec> embeds = JobUser.getJobsFromUser(user.get(), server, lg);
                    for (EmbedCreateSpec embed : embeds)
                        message.getChannel().flatMap(chan -> chan.createEmbed(embed)).subscribe();
                }
            }
            // Data recording
            else if((m = Pattern.compile("-list|(-all|\\p{L}+(?:\\s+\\p{L}+)*)\\s+(\\d{1,3})(\\s+.+)?").matcher(content)).matches()) {
                if (user.isPresent() && message.getAuthor().isPresent() && user.get().getId().equals(message.getAuthor().get().getId())) {
                    if (!m.group(0).equals("-list")) {
                        // Data Parsing and exceptions processing
                        Set<Job> jobs;